Skip to content

Instantly share code, notes, and snippets.

@anonymousik
Created March 23, 2026 12:41
Show Gist options
  • Select an option

  • Save anonymousik/da8291c39593ce92b125627bc15f9859 to your computer and use it in GitHub Desktop.

Select an option

Save anonymousik/da8291c39593ce92b125627bc15f9859 to your computer and use it in GitHub Desktop.
smarttube_cfg_tweaker
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
smarttube_config.py
================================================================
PlayBox Titanium -- SmartTube Config Manager
Eksport / Optymalizacja / Import konfiguracji SmartTube
Uzycie:
python3 smarttube_config.py --device 192.168.1.3:5555
python3 smarttube_config.py --dry-run
python3 smarttube_config.py --export-only
python3 smarttube_config.py --validate
python3 smarttube_config.py --restore ./SmartTubeBackup_original.zip
python3 smarttube_config.py --show ./SmartTubeBackup.zip
================================================================
"""
from __future__ import annotations
import argparse
import io
import json
import shutil
import subprocess
import sys
import time
import zipfile
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Tuple
# ================================================================
# Kolory (ASCII only -- Termux safe)
# ================================================================
_R = "\033[0m"; _G = "\033[92m"; _Y = "\033[93m"
_B = "\033[94m"; _E = "\033[91m"; _H = "\033[95m"
_C = "\033[96m"; _D = "\033[2m"; _BO = "\033[1m"
def ok(m): print(_G + " [OK] " + str(m) + _R)
def warn(m): print(_Y + " [!!] " + str(m) + _R)
def err(m): print(_E + " [XX] " + str(m) + _R)
def info(m): print(_B + " [ ] " + str(m) + _R)
def fix(m): print(_Y + " [>>] " + str(m) + _R)
def hdr(m): print("\n" + _H + _BO + "=" * 62 + "\n " + str(m) + "\n" + "=" * 62 + _R)
def sub(m): print("\n" + _C + " -- " + str(m) + " --" + _R)
def step(n, t, m): print(_BO + _B + "\n [" + str(n) + "/" + str(t) + "] " + m + _R)
# ================================================================
# Stale
# ================================================================
PKG_STABLE = "org.smarttube.stable"
PKG_BETA = "org.smarttube.beta"
PKG_LEGACY = "com.liskovsoft.smarttubetv"
DEVICE_BACKUP_ZIP = "/sdcard/SmartTubeBackup.zip"
RESTORE_BROADCAST = "com.liskovsoft.smarttube.RESTORE"
MIN_BACKUP_SIZE_BYTES = 10 * 1024 # 10 KB
LOCAL_DIR = Path.home() / ".playbox_cache" / "smarttube_configs"
LAUNCH_TIMEOUT = 20
# ================================================================
# ADB wrapper
# ================================================================
class _ADB:
dev: Optional[str] = None
_TO = 30
@classmethod
def connect(cls, target: str) -> bool:
try:
r = subprocess.run(
["adb", "connect", target],
capture_output=True, text=True, timeout=10
)
if "connected" in r.stdout.lower():
cls.dev = target
ok("ADB: " + target)
return True
err("ADB connect failed: " + r.stdout.strip())
return False
except FileNotFoundError:
err("'adb' nie znalezione -- zainstaluj Android Platform Tools")
sys.exit(1)
except subprocess.TimeoutExpired:
err("ADB timeout: " + target)
return False
@classmethod
def autodetect(cls) -> bool:
try:
out = subprocess.check_output(
["adb", "devices"], text=True, timeout=5
)
for line in out.splitlines():
if "\tdevice" in line:
cls.dev = line.split("\t")[0].strip()
ok("ADB auto-wykryty: " + cls.dev)
return True
except Exception:
pass
return False
@classmethod
def sh(cls, cmd: str, silent: bool = False) -> str:
if not cls.dev:
return ""
try:
return subprocess.check_output(
["adb", "-s", cls.dev, "shell", cmd],
stderr=subprocess.STDOUT, text=True, timeout=cls._TO
).strip()
except subprocess.TimeoutExpired:
if not silent:
warn("Timeout: " + cmd[:60])
return ""
except subprocess.CalledProcessError as e:
return (e.output or "").strip()
except Exception as e:
if not silent:
err(str(e))
return ""
@classmethod
def pull(cls, remote: str, local: str) -> bool:
try:
subprocess.check_call(
["adb", "-s", cls.dev, "pull", remote, local],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
timeout=120
)
return Path(local).exists()
except Exception:
return False
@classmethod
def push(cls, local: str, remote: str) -> bool:
try:
subprocess.check_call(
["adb", "-s", cls.dev, "push", local, remote],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
timeout=120
)
return True
except Exception:
return False
@classmethod
def pkg_exists(cls, pkg: str) -> bool:
return pkg in cls.sh("pm list packages " + pkg, silent=True)
@classmethod
def pkg_running(cls, pkg: str) -> bool:
out = cls.sh("pidof " + pkg + " 2>/dev/null", silent=True)
return bool(out and out.strip().split()[0].isdigit())
@classmethod
def file_exists(cls, path: str) -> bool:
return "yes" in cls.sh("test -f " + path + " && echo yes", silent=True)
@classmethod
def file_size(cls, path: str) -> int:
out = cls.sh("stat -c %s " + path + " 2>/dev/null", silent=True)
try:
return int(out.strip())
except (ValueError, AttributeError):
return 0
@classmethod
def file_mtime(cls, path: str) -> str:
return cls.sh("stat -c '%y' " + path + " 2>/dev/null", silent=True)
@classmethod
def pkg_version_code(cls, pkg: str) -> str:
out = cls.sh(
"dumpsys package " + pkg + " | grep versionCode",
silent=True
)
for part in (out or "").split():
if part.startswith("versionCode="):
return part.split("=")[1]
return ""
def detect_pkg() -> Optional[str]:
for pkg in (PKG_STABLE, PKG_BETA, PKG_LEGACY):
if _ADB.pkg_exists(pkg):
return pkg
return None
# ================================================================
# WALIDACJA backupu
# ================================================================
class BackupValidator:
@dataclass
class Result:
ok: bool
size_bytes: int
mtime: str
zip_valid: bool
has_prefs: bool
prefs_files: List[str]
has_databases: bool
db_files: List[str]
all_entries: List[str]
version_in_zip: str
warnings: List[str]
errors: List[str]
@classmethod
def validate_local(cls, zip_path: Path):
warns = []
errors = []
prefs = []
dbs = []
all_e = []
ver = ""
size = zip_path.stat().st_size if zip_path.exists() else 0
if not zip_path.exists():
errors.append("Plik nie istnieje: " + str(zip_path))
return cls._make(False, 0, "", False, False, [], False, [], [], "", warns, errors)
if size < MIN_BACKUP_SIZE_BYTES:
errors.append("Plik za maly: " + str(size) + " B (min " + str(MIN_BACKUP_SIZE_BYTES) + " B)")
if not zipfile.is_zipfile(str(zip_path)):
errors.append("Plik nie jest poprawnym ZIP (uszkodzony?)")
return cls._make(False, size, "", False, False, [], False, [], [], "", warns, errors)
try:
with zipfile.ZipFile(str(zip_path), "r") as zf:
bad = zf.testzip()
if bad:
errors.append("Uszkodzony plik w ZIP: " + bad)
all_e = zf.namelist()
# POPRAWKA: szukaj gdziekolwiek w sciezce wewnatrz zipa
prefs = [n for n in all_e if "shared_prefs/" in n and n.endswith(".xml")]
dbs = [n for n in all_e if "databases/" in n and n.endswith(".db")]
for candidate in ("META-INF/MANIFEST.MF", "version.txt", "manifest.xml"):
if candidate in all_e:
try:
content = zf.read(candidate).decode("utf-8", errors="replace")
for line in content.splitlines():
if "version" in line.lower() or "versionCode" in line:
ver = line.strip()[:40]
break
except Exception:
pass
except zipfile.BadZipFile as e:
errors.append("Blad odczytu ZIP: " + str(e))
return cls._make(False, size, "", True, False, [], False, [], [], "", warns, errors)
has_prefs = len(prefs) > 0
has_dbs = len(dbs) > 0
if not has_prefs:
errors.append("Brak shared_prefs/*.xml -- backup niekompletny")
oauth_suspects = [n for n in all_e if any(kw in n.lower() for kw in ("token", "oauth", "auth", "account", "credential"))]
if oauth_suspects:
warns.append("Znaleziono potencjalne pliki auth: " + ", ".join(oauth_suspects[:3]) + " -- sprawdz recznie przed udostepnieniem backupu")
mtime = datetime.fromtimestamp(zip_path.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S")
age_days = (datetime.now() - datetime.fromtimestamp(zip_path.stat().st_mtime)).days
if age_days > 30:
warns.append("Backup ma " + str(age_days) + " dni -- rozważ odswiezenie")
is_ok = len(errors) == 0
return cls._make(is_ok, size, mtime, True, has_prefs, prefs, has_dbs, dbs, all_e, ver, warns, errors)
@classmethod
def validate_device(cls, pkg: str) -> dict:
result = {
"exists": False,
"size": 0,
"mtime": "",
"zip_ok": False,
"contents": [],
"warnings": [],
"errors": [],
}
if not _ADB.file_exists(DEVICE_BACKUP_ZIP):
result["errors"].append("Brak pliku: " + DEVICE_BACKUP_ZIP)
return result
result["exists"] = True
result["size"] = _ADB.file_size(DEVICE_BACKUP_ZIP)
result["mtime"] = _ADB.file_mtime(DEVICE_BACKUP_ZIP)
if result["size"] < MIN_BACKUP_SIZE_BYTES:
result["errors"].append("Plik za maly: " + str(result["size"]) + " B")
return result
contents_raw = _ADB.sh("unzip -l " + DEVICE_BACKUP_ZIP + " 2>/dev/null", silent=True)
if contents_raw:
result["zip_ok"] = True
for line in contents_raw.splitlines():
parts = line.strip().split()
if len(parts) >= 4 and parts[-1] not in ("Name", "----"):
result["contents"].append(parts[-1])
app_ver = _ADB.pkg_version_code(pkg)
if app_ver:
result["app_version_code"] = app_ver
return result
@classmethod
def _make(cls, ok, size, mtime, zip_valid, has_prefs, prefs, has_dbs, dbs, all_e, ver, warns, errors):
return {
"ok": ok,
"size_bytes": size,
"mtime": mtime,
"zip_valid": zip_valid,
"has_prefs": has_prefs,
"prefs_files": prefs,
"has_databases": has_dbs,
"db_files": dbs,
"all_entries": all_e,
"version_in_zip": ver,
"warnings": warns,
"errors": errors,
}
@classmethod
def print_report(cls, r: dict, label: str = "") -> None:
sub("Walidacja backupu" + (" -- " + label if label else ""))
stat_ok = r.get("ok", False)
size = r.get("size_bytes", 0)
mtime = r.get("mtime", "?")
_s = _G if stat_ok else _E
print(" Status : " + _s + ("OK" if stat_ok else "BLAD") + _R)
print(" Rozmiar : " + str(size) + " B" +
(" (" + _G + "OK" + _R + ")" if size >= MIN_BACKUP_SIZE_BYTES
else " (" + _E + "ZA MALY" + _R + ")"))
print(" Data : " + str(mtime))
print(" ZIP valid : " + (_G + "TAK" + _R if r.get("zip_valid") else _E + "NIE" + _R))
prefs = r.get("prefs_files", [])
print(" shared_prefs: " + (_G if prefs else _E) + str(len(prefs)) + " plikow" + _R)
for p in prefs:
print(" " + _C + p + _R)
dbs = r.get("db_files", [])
if dbs:
print(" databases : " + str(len(dbs)) + " plikow")
for d in dbs:
print(" " + _C + d + _R)
ver = r.get("version_in_zip", "")
if ver:
print(" Wersja : " + _D + ver + _R)
app_ver = r.get("app_version_code", "")
if app_ver:
print(" App ver : " + _D + "versionCode=" + app_ver + _R)
warns = r.get("warnings", [])
errors = r.get("errors", [])
for w in warns: warn(w)
for e in errors: err(e)
if not warns and not errors:
ok("Backup kompletny i poprawny")
print()
info("UWAGA: Tokeny OAuth/konto Google NIE sa w backupie")
info(" Po przywroceniu wymagane jest ponowne logowanie do konta")
# ================================================================
# KROK 1 -- Eksport i walidacja
# ================================================================
class ConfigExporter:
@classmethod
def export(cls, pkg: str, local_dir: Path) -> Optional[Path]:
hdr("KROK 1 -- Eksport SmartTubeBackup.zip")
local_dir.mkdir(parents=True, exist_ok=True)
if _ADB.file_exists(DEVICE_BACKUP_ZIP):
size = _ADB.file_size(DEVICE_BACKUP_ZIP)
mtime = _ADB.file_mtime(DEVICE_BACKUP_ZIP)
ok("Znaleziono: " + DEVICE_BACKUP_ZIP)
info(" Rozmiar: " + str(size) + " B")
info(" Data : " + mtime)
if size < MIN_BACKUP_SIZE_BYTES:
warn("Plik wydaje sie za maly (" + str(size) + " B)")
warn("Moz byc uszkodzony -- generuje nowy")
if not cls._trigger_backup(pkg):
warn("Nie mozna wygenerowac nowego -- uzywam istniejacego")
else:
warn("Brak pliku: " + DEVICE_BACKUP_ZIP)
info("Uruchamiam SmartTube aby wygenerowac backup...")
if not cls._trigger_backup(pkg):
err("Nie mozna wygenerowac backupu")
cls._print_manual_hint()
return None
local_zip = local_dir / "SmartTubeBackup_original.zip"
info("Pobieranie: " + DEVICE_BACKUP_ZIP + " -> " + str(local_zip))
if not _ADB.pull(DEVICE_BACKUP_ZIP, str(local_zip)):
err("Pull nieudany: " + DEVICE_BACKUP_ZIP)
return None
ok("Pobrano: " + str(local_zip) + " (" + str(local_zip.stat().st_size) + " B)")
v = BackupValidator.validate_local(local_zip)
BackupValidator.print_report(v, "oryginál")
if not v["ok"]:
err("Backup niepoprawny -- nie mozna kontynuowac")
return None
return local_zip
@classmethod
def _trigger_backup(cls, pkg: str) -> bool:
_ADB.sh("am force-stop " + pkg, silent=True)
time.sleep(1)
export_actions = [
"com.liskovsoft.smarttube.BACKUP",
"com.liskovsoft.smartyoutubetv2.BACKUP_EXPORT",
"com.liskovsoft.smarttubetv.EXPORT",
]
for action in export_actions:
out = _ADB.sh("am broadcast -a " + action + " -p " + pkg + " 2>/dev/null", silent=True)
if "result=0" in out or "Broadcast completed" in out:
ok(" Broadcast: " + action)
time.sleep(3)
if _ADB.file_exists(DEVICE_BACKUP_ZIP):
if _ADB.file_size(DEVICE_BACKUP_ZIP) > MIN_BACKUP_SIZE_BYTES:
ok(" Backup wygenerowany")
return True
info(" Uruchamiam SmartTube (zapisuje backup przy starcie)...")
_ADB.sh("monkey -p " + pkg + " -c android.intent.category.LAUNCHER 1 2>/dev/null", silent=True)
for i in range(LAUNCH_TIMEOUT):
time.sleep(1)
print(" Czekam na SmartTubeBackup.zip... " + str(i + 1) + "/" + str(LAUNCH_TIMEOUT) + "s", end="\r")
if _ADB.file_exists(DEVICE_BACKUP_ZIP):
if _ADB.file_size(DEVICE_BACKUP_ZIP) > MIN_BACKUP_SIZE_BYTES:
print()
ok(" Backup wygenerowany po " + str(i + 1) + "s")
time.sleep(2)
_ADB.sh("am force-stop " + pkg, silent=True)
return True
print()
_ADB.sh("am force-stop " + pkg, silent=True)
time.sleep(2)
if _ADB.file_exists(DEVICE_BACKUP_ZIP):
if _ADB.file_size(DEVICE_BACKUP_ZIP) > MIN_BACKUP_SIZE_BYTES:
ok(" Backup wygenerowany przy zamknieciu")
return True
return False
@classmethod
def _print_manual_hint(cls) -> None:
print()
warn("Recznie wygeneruj backup w SmartTube:")
print(" " + _Y + " 1. Otworz SmartTube" + _R)
print(" " + _Y + " 2. Ustawienia -> Kopia zapasowa -> Utworz kopie zapasowa" + _R)
print(" " + _Y + " 3. Uruchom ponownie: python3 smarttube_config.py" + _R)
# ================================================================
# Baza optymalnych ustawien dla BCM72604
# ================================================================
BCM72604_OPTIMAL: Dict[str, Tuple[str, str, str]] = {
"video_codec": ("int", "2", "VP9 -- jedyny HW codec BCM72604"),
"av1_enabled": ("boolean", "false", "AV1 off -- brak HW, 100% CPU A15"),
"vp9_enabled": ("boolean", "true", "VP9 on -- VDec32 HW path"),
"preferred_codec": ("string", "vp9", "Preferowany VP9"),
"codec_black_list": ("string", "av1", "AV1 na czarnej liscie"),
"use_tunnel_mode": ("boolean", "true", "Tunnel -- hardware clock sync BCM"),
"tunnel_mode_enabled": ("boolean", "true", "Tunnel mode enabled"),
"tunneled_playback": ("boolean", "true", "Tunneled playback"),
"video_resolution": ("string", "1080", "Max 1080p"),
"preferred_resolution": ("int", "4", "1080p index"),
"max_video_resolution": ("string", "1080", "Max 1080p"),
"default_quality": ("string", "hd1080","HD 1080p domyslnie"),
"hdr_mode": ("boolean", "true", "HDR10 wlaczony"),
"hdr_enabled": ("boolean", "true", "HDR enabled"),
"hdr10_enabled": ("boolean", "true", "HDR10 enabled"),
"video_frame_rate_switch": ("boolean", "true", "Auto fps switch"),
"auto_frame_rate": ("boolean", "true", "Automatyczna czestotliwosc"),
"frame_rate_switching": ("boolean", "true", "Przelaczanie fps"),
"switch_frame_rate": ("boolean", "true", "Przelacz fps do tresci"),
"player_data_source": ("int", "0", "ExoPlayer (nie MediaPlayer)"),
"buffer_type": ("int", "2", "Duzy bufor"),
"min_buffer_duration": ("string", "15000", "Min bufor 15s"),
"max_buffer_duration": ("string", "50000", "Max bufor 50s"),
"playback_buffer_ms": ("string", "2500", "Bufor do startu 2.5s"),
"back_buffer_ms": ("string", "10000", "Bufor cofania 10s"),
"hardware_acceleration": ("boolean", "true", "HW acceleration"),
"hw_acceleration": ("boolean", "true", "HW acceleration"),
"use_hardware_decoder": ("boolean", "true", "HW dekoder"),
"background_play_mode": ("int", "0", "Brak odtwarzania w tle"),
"background_playback": ("boolean", "false", "Background off"),
"player_stats": ("boolean", "false", "Stats overlay off"),
"show_stats": ("boolean", "false", "Statystyki off"),
"debug_view": ("boolean", "false", "Debug view off"),
"audio_delay_ms": ("int", "0", "Brak opoznienia audio"),
"frame_interpolation": ("boolean", "false", "Interpolacja off"),
"smooth_playback": ("boolean", "false", "Smooth off"),
"abr_enabled": ("boolean", "true", "ABR wlaczony"),
"remember_quality": ("boolean", "true", "Pamietaj jakosc"),
"force_default_quality": ("boolean", "false", "Nie wymuszaj domyslnej"),
"speed_buttons": ("boolean", "true", "Przyciski szybkosci"),
"default_speed": ("float", "1.0", "Szybkosc 1.0x"),
"suggestions_enabled": ("boolean", "false", "Sugestie off (CPU)"),
"feed_enabled": ("boolean", "false", "Feed off"),
"autoplay_enabled": ("boolean", "true", "Autoplay on"),
"subtitles_font_size": ("int", "16", "Rozmiar napisow TV"),
"live_stream_quality": ("string", "1080", "Live 1080p"),
}
# ================================================================
# KROK 2 -- Optymalizacja ZIP
# ================================================================
class ZipOptimizer:
def __init__(self, source_zip: Path, dry_run: bool = False):
self.source = source_zip
self.dry_run = dry_run
self.changes_by_file: Dict[str, List[Tuple[str, str, str]]] = {}
def _optimize_xml_bytes(self, data: bytes, entry_name: str) -> Tuple[bytes, int]:
changes = []
try:
root = ET.fromstring(data.decode("utf-8", errors="replace"))
except ET.ParseError as e:
warn(" XML parse error w " + entry_name + ": " + str(e))
return data, 0
def _set(key, typ, value):
for el in root:
if el.get("name") == key:
old = el.text if el.tag == "string" else el.get("value", "")
if str(old) == str(value) and el.tag == typ:
return
if typ == "string":
el.tag = "string"
el.text = value
if "value" in el.attrib:
del el.attrib["value"]
else:
el.tag = typ
el.set("value", value)
el.text = None
changes.append((key, str(old), value))
return
if typ == "string":
ne = ET.SubElement(root, "string", {"name": key})
ne.text = value
else:
ET.SubElement(root, typ, {"name": key, "value": value})
changes.append((key, "<brak>", value))
for key, (typ, value, _desc) in BCM72604_OPTIMAL.items():
_set(key, typ, value)
for el in root:
name = el.get("name", "")
val = el.text if el.tag == "string" else el.get("value", "")
if val and val in ["2160", "4k", "4K", "UHD", "2160p", "5", "6", "7"]:
if any(kw in name.lower() for kw in ("resolution", "quality")):
old = val
if el.tag == "string":
el.text = "1080"
else:
el.set("value", "4")
changes.append((name, old, "1080(4K->1080p)"))
if "av1" in name.lower() and val not in ("false", "0", ""):
old = val
if el.tag == "boolean":
el.set("value", "false")
elif el.tag == "string":
el.text = "false"
elif el.tag == "int":
el.set("value", "0")
changes.append((name, old, "disabled(av1)"))
if not changes:
return data, 0
_indent_xml(root)
buf = io.BytesIO()
ET.ElementTree(root).write(buf, encoding="utf-8", xml_declaration=True)
return buf.getvalue(), len(changes)
def optimize(self, output_zip: Path) -> bool:
hdr("KROK 2 -- Optymalizacja shared_prefs w ZIP")
if not zipfile.is_zipfile(str(self.source)):
err("Zrodlowy ZIP niepoprawny")
return False
total_changes = 0
total_xml = 0
try:
with zipfile.ZipFile(str(self.source), "r") as zin:
entries = zin.infolist()
info("Plikow w ZIP: " + str(len(entries)))
if self.dry_run:
for entry in entries:
if entry.is_dir():
continue
fname = entry.filename
raw = zin.read(fname)
# POPRAWKA: szukaj gdziekolwiek w sciezce
if "shared_prefs/" in fname and fname.endswith(".xml"):
_, n = self._optimize_xml_bytes(raw, fname)
if n > 0:
fix(" [DRY] " + fname + ": " + str(n) + " zmian")
total_changes += n
else:
ok(" [DRY] " + fname + ": brak zmian")
total_xml += 1
else:
info(" [DRY] " + _D + fname + " (kopia bez zmian)" + _R)
info("Lacznie: " + str(total_changes) + " potencjalnych zmian w " + str(total_xml) + " plikach XML")
return True
with zipfile.ZipFile(str(output_zip), "w", compression=zipfile.ZIP_DEFLATED) as zout:
for entry in entries:
raw = zin.read(entry.filename)
fname = entry.filename
if entry.is_dir():
zout.mkdir(fname)
continue
# POPRAWKA: szukaj gdziekolwiek w sciezce
if "shared_prefs/" in fname and fname.endswith(".xml"):
info(" XML: " + fname)
new_raw, n = self._optimize_xml_bytes(raw, fname)
if n > 0:
fix(" " + str(n) + " zmian")
self.changes_by_file[fname] = []
total_changes += n
total_xml += 1
else:
ok(" brak zmian -- juz optymalny")
zout.writestr(entry, new_raw)
elif "databases/" in fname and fname.endswith(".db"):
info(" DB: " + fname + _D + " (binarna -- kopia bez zmian)" + _R)
zout.writestr(entry, raw)
else:
info(" " + _D + fname + " (kopia bez zmian)" + _R)
zout.writestr(entry, raw)
except zipfile.BadZipFile as e:
err("Blad ZIP: " + str(e))
return False
except Exception as e:
err("Blad przetwarzania: " + str(e))
import traceback
traceback.print_exc()
return False
print()
ok("Optymalizacja ZIP zakonczona:")
ok(" Zmian lacznic : " + str(total_changes))
ok(" Plikow XML : " + str(total_xml))
ok(" Wynikowy ZIP : " + str(output_zip) + " (" + str(output_zip.stat().st_size) + " B)")
return True
def print_changes(self) -> None:
if not self.changes_by_file:
return
sub("Zmienione pliki")
for fname, changes in self.changes_by_file.items():
print(" " + _BO + fname + _R)
for key, old, new in changes:
ks = (key[:32] + "..") if len(key) > 34 else key.ljust(34)
print(" " + _C + ks + _R + " " + _D + str(old)[:10].ljust(10) + _R + " -> " + _G + str(new)[:14] + _R)
# ================================================================
# KROK 3 -- Import do SmartTube (Zaktualizowany system uderzeniowy A+B)
# ================================================================
class ConfigImporter:
"""
Importuje zoptymalizowany plik XML i ZIP do SmartTube.
Wdrożono dwukanałowy proces wypychania dla maksymalnej stabilności.
"""
@classmethod
def import_zip(cls, pkg: str, optimized_zip: Path, dry_run: bool = False) -> bool:
hdr("KROK 3 -- Wgrywanie konfiguracji do SmartTube")
if dry_run:
info("[DRY RUN] Import pominieto")
return True
local_size = optimized_zip.stat().st_size
remote_backup_dir = "/sdcard/data/" + pkg + "/backup"
info("Cel A (glowny) : " + remote_backup_dir)
info("Cel B (zapasowy): " + DEVICE_BACKUP_ZIP)
print()
v = BackupValidator.validate_local(optimized_zip)
if not v["ok"]:
err("Zoptymalizowany ZIP nie przeszedl walidacji")
for e in v["errors"]:
err(" " + e)
return False
_ADB.sh("am force-stop " + pkg, silent=True)
time.sleep(1.5)
ok("SmartTube zatrzymany")
print("\n " + _C + "-- Kanal A -- wypakowanie do katalogu importu --" + _R)
unpack_dir = optimized_zip.parent / "_unpack_tmp"
unpack_dir.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(str(optimized_zip), "r") as zf:
zf.extractall(str(unpack_dir))
_ADB.sh("mkdir -p " + remote_backup_dir, silent=True)
ok("Katalog importu: " + remote_backup_dir)
pushed_count = 0
for item in unpack_dir.rglob("*"):
if item.is_file():
size = item.stat().st_size
remote_file = remote_backup_dir + "/" + item.name
print(" " + _B + "[ ]" + _R + " Push: " + item.name + " (" + str(size) + " B) -> " + remote_file)
_ADB.push(str(item), remote_file)
r_size = _ADB.file_size(remote_file)
if r_size == size:
print(" " + _G + "[OK]" + _R + " OK -- " + str(size) + " B")
pushed_count += 1
else:
print(" " + _E + "[XX]" + _R + " BLAD -- " + str(r_size) + " B != " + str(size) + " B")
ok("Wgrano : " + str(pushed_count) + " plikow")
ok("Sciezka : " + remote_backup_dir)
ls_out = _ADB.sh("ls -la " + remote_backup_dir, silent=True)
if ls_out:
for line in ls_out.splitlines():
print(" " + _D + line + _R)
shutil.rmtree(str(unpack_dir), ignore_errors=True)
print("\n " + _C + "-- Kanal B -- push ZIP jako SmartTubeBackup.zip --" + _R)
_ADB.push(str(optimized_zip), DEVICE_BACKUP_ZIP)
zip_r_size = _ADB.file_size(DEVICE_BACKUP_ZIP)
if zip_r_size == local_size:
ok("ZIP na urzadzeniu: " + str(zip_r_size) + " B")
else:
err("Blad rozmiaru ZIP na urzadzeniu: " + str(zip_r_size) + " B")
print("\n " + _C + "-- Restarty systemowe --" + _R)
info("Wysylanie intentu przywrocenia (Broadcast RESTORE)...")
broadcast_cmd = "am broadcast -a " + RESTORE_BROADCAST + " --es path " + DEVICE_BACKUP_ZIP + " " + pkg + " 2>/dev/null"
_ADB.sh(broadcast_cmd, silent=True)
info("Testowy rozruch (wczytanie wstrzyknietych wlasciwosci)...")
_ADB.sh("monkey -p " + pkg + " -c android.intent.category.LAUNCHER 1 2>/dev/null", silent=True)
time.sleep(4)
info("Twardy reset procesu (dla finalizacji)...")
_ADB.sh("am force-stop " + pkg, silent=True)
time.sleep(2)
_ADB.sh("monkey -p " + pkg + " -c android.intent.category.LAUNCHER 1 2>/dev/null", silent=True)
time.sleep(3)
if _ADB.pkg_running(pkg):
ok("Proces SmartTube stabilny po imporcie.")
else:
warn("Uruchom SmartTube recznie.")
print()
sub("Instrukcje post-wdrożeniowe")
print(" " + _G + _BO + "Aplikacja wczyta nowy zestaw zasad. Zweryfikuj:" + _R)
print(" " + _C + " - Kodek: VP9 | HDR: Włączony" + _R)
print(" " + _Y + "UWAGA: Konieczne ponowne zalogowanie do konta Google!" + _R)
return True
# ================================================================
# Pomocnicze
# ================================================================
def _indent_xml(elem: ET.Element, level: int = 0) -> None:
indent = "\n" + " " * level
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = indent + " "
if not elem.tail or not elem.tail.strip():
elem.tail = indent
last = None
for child in elem:
_indent_xml(child, level + 1)
last = child
if last is not None and (not last.tail or not last.tail.strip()):
last.tail = indent
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = indent
def print_zip_summary(zip_path: Path) -> None:
KEYS = [
"video_codec", "use_tunnel_mode", "hdr_mode",
"video_resolution", "auto_frame_rate",
"hardware_acceleration", "av1_enabled", "vp9_enabled",
"buffer_type", "background_play_mode", "player_stats",
]
if not zip_path.exists() or not zipfile.is_zipfile(str(zip_path)):
return
print(" " + _BO + zip_path.name + ":" + _R)
xml_shown = 0
try:
with zipfile.ZipFile(str(zip_path), "r") as zf:
for entry in zf.infolist():
if entry.is_dir():
continue
fname = entry.filename
# POPRAWKA: szukaj gdziekolwiek w sciezce
if not ("shared_prefs/" in fname and fname.endswith(".xml")):
continue
raw = zf.read(fname)
found = {}
try:
root = ET.fromstring(raw.decode("utf-8", errors="replace"))
for el in root:
name = el.get("name", "")
if name in KEYS:
val = el.text if el.tag == "string" else el.get("value", "?")
found[name] = (el.tag, val)
except Exception:
pass
if found:
print(" " + _D + " [" + fname + "]" + _R)
for key in KEYS:
if key in found:
typ, val = found[key]
opt = BCM72604_OPTIMAL.get(key, (None, None, None))[1]
col = _G if (opt and str(val) == str(opt)) else (_E if opt else _B)
print(" " + _C + key[:34].ljust(34) + _R + " = " + col + str(val) + _R + _D + " [" + typ + "]" + _R)
xml_shown += 1
except Exception as e:
warn("Nie mozna odczytac ZIP: " + str(e))
if xml_shown == 0:
warn(" Brak plikow shared_prefs/*.xml w " + zip_path.name)
print()
# ================================================================
# Glowny menedzer
# ================================================================
class SmartTubeConfigManager:
@classmethod
def run_full(cls,
device: Optional[str] = None,
dry_run: bool = False,
export_only: bool = False,
validate_only:bool = False,
restore_zip: Optional[str] = None) -> bool:
hdr("SmartTube Config Manager -- BCM72604 Optimizer")
info("Backup path: " + DEVICE_BACKUP_ZIP)
info("Tryb: " + ("DRY RUN" if dry_run
else "VALIDATE" if validate_only
else "RESTORE" if restore_zip
else "EXPORT ONLY" if export_only
else "PELNY"))
if device:
if not _ADB.connect(device):
return False
else:
if not _ADB.autodetect():
err("Brak polaczenia ADB -- podaj --device IP:PORT")
return False
pkg = detect_pkg()
if not pkg:
err("SmartTube nie zainstalowane")
return False
ok("Pakiet: " + pkg)
if validate_only:
sub("Walidacja na urzadzeniu")
rv = BackupValidator.validate_device(pkg)
BackupValidator.print_report(rv, "urzadzenie")
return rv.get("ok", False)
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
work_dir = LOCAL_DIR / ts
work_dir.mkdir(parents=True, exist_ok=True)
info("Katalog roboczy: " + str(work_dir))
if restore_zip:
return cls._do_restore(pkg, Path(restore_zip), dry_run)
step(1, 3, "Eksport SmartTubeBackup.zip z urzadzenia")
original_zip = ConfigExporter.export(pkg, work_dir / "original")
if original_zip is None:
return False
sub("Stan konfiguracji PRZED optymalizacja")
print_zip_summary(original_zip)
if export_only:
ok("Eksport zakonczone")
info("Plik: " + str(original_zip))
return True
step(2, 3, "Optymalizacja shared_prefs dla BCM72604")
optimized_zip = work_dir / "SmartTubeBackup.zip"
optimizer = ZipOptimizer(original_zip, dry_run=dry_run)
if not optimizer.optimize(optimized_zip):
err("Optymalizacja nieudana")
return False
if not dry_run:
sub("Stan konfiguracji PO optymalizacji")
print_zip_summary(optimized_zip)
v2 = BackupValidator.validate_local(optimized_zip)
BackupValidator.print_report(v2, "zoptymalizowany")
if not v2["ok"]:
err("Zoptymalizowany ZIP nie przeszedl walidacji")
return False
step(3, 3, "Import do SmartTube")
if not dry_run:
try:
ans = input("\n " + _Y + "Wyslac zoptymalizowany ZIP i wykonac import? [T/n] > " + _R).strip().lower()
proceed = ans in ("", "t", "y", "tak", "yes")
except (EOFError, KeyboardInterrupt):
proceed = False
if proceed:
if ConfigImporter.import_zip(pkg, optimized_zip, dry_run):
hdr("Gotowe!")
ok("Konfiguracja SmartTube zoptymalizowana dla BCM72604 ✓")
info("Oryginalna kopia: " + str(original_zip))
info("Rollback: --restore " + str(original_zip))
return True
return False
else:
info("Import anulowany")
info("Plik gotowy: " + str(optimized_zip))
info("Recznie: --restore " + str(optimized_zip))
else:
info("[DRY RUN] Wszystkie kroki przeanalizowane bez zapisu")
return True
@classmethod
def _do_restore(cls, pkg: str, zip_path: Path, dry_run: bool) -> bool:
hdr("RESTORE -- Przywracanie konfiguracji")
if not zip_path.exists():
err("Plik nie istnieje: " + str(zip_path))
return False
if not zipfile.is_zipfile(str(zip_path)):
err("Nie jest poprawnym ZIP: " + str(zip_path))
return False
v = BackupValidator.validate_local(zip_path)
BackupValidator.print_report(v, "restore")
if not v["ok"]:
err("Plik nie przeszedl walidacji -- przerywam")
return False
ok("Przywracam: " + str(zip_path))
return ConfigImporter.import_zip(pkg, zip_path, dry_run)
# ================================================================
# CLI
# ================================================================
def main() -> None:
parser = argparse.ArgumentParser(
description="SmartTube Config Manager -- BCM72604 Optimizer",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"PRZYKLADY:\n"
" python3 smarttube_config.py --device 192.168.1.3:5555\n"
" python3 smarttube_config.py --dry-run\n"
" python3 smarttube_config.py --validate\n"
" python3 smarttube_config.py --export-only\n"
" python3 smarttube_config.py --show ./SmartTubeBackup.zip\n"
" python3 smarttube_config.py --restore ./SmartTubeBackup_original.zip\n"
)
)
parser.add_argument("--device", default=None)
parser.add_argument("--dry-run", action="store_true", help="Pokaz zmiany bez zapisu i importu")
parser.add_argument("--export-only", action="store_true", help="Tylko pobierz ZIP")
parser.add_argument("--validate", action="store_true", help="Tylko sprawdz backup na urzadzeniu")
parser.add_argument("--restore", default=None, metavar="ZIP", help="Przywroc z lokalnego pliku ZIP")
parser.add_argument("--show", default=None, metavar="ZIP", help="Pokaz kluczowe ustawienia z pliku ZIP")
args = parser.parse_args()
if args.show:
p = Path(args.show)
if not p.exists():
err("Plik nie istnieje: " + args.show)
sys.exit(1)
hdr("Zawartosc: " + p.name)
v = BackupValidator.validate_local(p)
BackupValidator.print_report(v, p.name)
print_zip_summary(p)
sys.exit(0)
success = SmartTubeConfigManager.run_full(
device = args.device,
dry_run = args.dry_run,
export_only = args.export_only,
validate_only = args.validate,
restore_zip = args.restore,
)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()

Użycie

Pełny tryb

python3 smarttube_config.py --device 192.168.1.3:5555

Podgląd co zostałoby zmienione (bez zapisu)

python3 smarttube_config.py --dry-run --device 192.168.1.3:5555

Sprawdź zawartość istniejącego ZIP

python3 smarttube_config.py --show /ścieżka/SmartTubeBackup.zip

Rollback do oryginału

python3 smarttube_config.py
--restore ~/.playbox_cache/smarttube_configs/20240101_120000/original/SmartTubeBackup_original.zip

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment