Auteur Sujet: Installation de Connect TV d'SFR sur box Android TV  (Lu 53284 fois)

Empereur et 4 Invités sur ce sujet

Denis M

  • Abonné RED by SFR fibre FttH
  • *
  • Messages: 2 146
  • Sermaise 91530
Installation de Connect TV d'SFR sur box Android TV
« Réponse #144 le: 01 février 2026 à 16:09:25 »
Bonjour, je me permets d'intervenir sur ce forum car je cherchais une solution depuis longtemps et n'y arrivant pas j'avais laissé tomber. En effet, j'ai 2 stick amazon depuis 4 ans et je suis content. Je suis chez red sfr et je compte y rester. Moi, je souhaiterai avoir l'application red tv et faire comme à la maison, regarder ce que je veux dans mes autres chambres . En plus, je souhaite avoir cette application, aussi, pour pouvoir regarder la tv lorsque j'utilise ma clé en dehors de chez moi et donc sans être sur le réseau sfr. Enfin, j'ai une tv Panasonic et je n'arrive pas non plus à installer red tv et donc, si c'est possible, je souhaiterai aussi l'installer dessus mais ça, c'est moins important. Merci pour votre aide. David
Bonjour,

il suffit de lire le présent topic à la bonne page:

La solution à ce problème est simple : il suffit de re-signer les deux applications avec la même clé, ce que j'ai fait et qui ne m'a pris qu'une minute avec APKTool. Vous pouvez très facilement faire la même chose. Pour ceux qui ne veulent pas s'embêter et souhaitent directement récupérer les deux applications correctement signées, vous les trouverez ici.

Seule l'appli du milieu suffit, la 7.1.

Ça fonctionne sur tout ce qui est Android ou à base d'Android (FireStick), pas sur les engins exotiques tournant sous TiVo OS/MyHomeScreen comme certaines Panasonic.

Il existe des modèles civilisés utilisant des systèmes plus agréables, c'est expliqué ici.

SuperBaobab

  • Abonné SFR fibre FttH
  • *
  • Messages: 10
  • Lyon (69)
Installation de Connect TV d'SFR sur box Android TV
« Réponse #145 le: Hier à 22:56:28 »
Salut à tous,

Désolé je ne passe pas souvent mais j'essaie de le faire quand je peux.

J'ai analysé rapidement le fonctionnement de l'APK Update Manager, qui permet de télécharger et mettre à jour l'APK Connect TV, les appli de replay et TV (qui sont spécifiques SFR).

Du coup j'ai fait un petit script Python à l'arrache pour télécharger tous ces APKs sans avoir besoin d'un boitier Connect TV ou de Update Manager.

Ça ressemble à ça :


On peux voir d'ailleurs qu'une version 8.0.0 arrive (la 7.3.0 est toujours la dernière version stable déployée).

Et le script :
#!/usr/bin/env python3
"""
Téléchargeur d'APKs SFR Connect TV
Récupère les APKs depuis le CDN SFR Update Manager avec menu interactif.
"""

import json
import hashlib
import urllib.request
import urllib.error
import sys
from pathlib import Path

# ── Configuration ────────────────────────────────────────────────────────────
CDN_CONF_URL = (
    "https://k69977.cdn.sfr.net"
    "/CONF/ANDROIDTV/com.altice.androidtv.appsUpdateManager"
    "/conf/v2/update-manager-conf.json"
)
OUTPUT_DIR = Path(__file__).parent / "APKs"

# Modes à exclure (apps système non installables manuellement)
EXCLUDED_MODES = {"hidden"}
# ─────────────────────────────────────────────────────────────────────────────

MODE_COLORS = {
    "mandatory": "\033[92m",   # vert
    "promoted":  "\033[94m",   # bleu
    "recommended": "\033[96m", # cyan
    "hidden":    "\033[90m",   # gris
}
RESET  = "\033[0m"
BOLD   = "\033[1m"
DIM    = "\033[2m"
YELLOW = "\033[93m"
RED    = "\033[91m"
GREEN  = "\033[92m"


def clr(text, code): return f"{code}{text}{RESET}"


def fetch_json(url: str) -> list:
    req = urllib.request.Request(url, headers={"User-Agent": "SFR-UpdateManager/2.7.2"})
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read().decode("utf-8"))


def md5_file(path: Path) -> str:
    h = hashlib.md5()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            h.update(chunk)
    return h.hexdigest()


def human_size(n: int) -> str:
    for unit in ("o", "Ko", "Mo", "Go"):
        if n < 1024:
            return f"{n:.1f} {unit}"
        n /= 1024
    return f"{n:.1f} To"


def safe_name(name: str) -> str:
    return "".join(c if c.isalnum() or c in " ._-" else "_" for c in name).strip()


def group_by_app(catalog: list) -> dict[str, list]:
    """Regroupe toutes les entrées par appName, triées par versionCode décroissant."""
    groups: dict[str, list] = {}
    for entry in catalog:
        if not entry.get("binaries"):
            continue
        if entry.get("rules", {}).get("mode") in EXCLUDED_MODES:
            continue
        name = entry["appName"]
        groups.setdefault(name, []).append(entry)
    for name in groups:
        groups[name].sort(key=lambda e: e.get("versionCode", 0), reverse=True)
    return dict(sorted(groups.items()))


def ask(prompt: str) -> str:
    try:
        return input(prompt).strip()
    except (KeyboardInterrupt, EOFError):
        print()
        return ""


def print_header(title: str):
    print()
    print(clr("═" * 60, BOLD))
    print(clr(f"  {title}", BOLD))
    print(clr("═" * 60, BOLD))


def download_file(url: str, dest: Path, expected_md5: str, expected_size: int) -> bool:
    """Télécharge un fichier avec barre de progression et vérification MD5."""
    if dest.exists():
        if md5_file(dest) == expected_md5:
            print(clr(f"    ✓ Déjà téléchargé et valide : {dest.name}", GREEN))
            return True
        print(clr("    ⚠ Fichier corrompu, re-téléchargement...", YELLOW))
        dest.unlink()

    print(f"    {clr('↓', YELLOW)} {url}")
    print(f"    {clr('→', YELLOW)} {dest.name}  ({human_size(expected_size)})")

    dest.parent.mkdir(parents=True, exist_ok=True)
    tmp = dest.with_suffix(".tmp")

    try:
        req = urllib.request.Request(url, headers={"User-Agent": "SFR-UpdateManager/2.7.2"})
        with urllib.request.urlopen(req, timeout=120) as resp, open(tmp, "wb") as f:
            downloaded = 0
            while True:
                chunk = resp.read(65536)
                if not chunk:
                    break
                f.write(chunk)
                downloaded += len(chunk)
                if expected_size > 0:
                    pct = downloaded * 100 // expected_size
                    bar = clr("█" * (pct // 5), GREEN) + clr("░" * (20 - pct // 5), DIM)
                    print(f"\r    [{bar}] {pct:3d}%  {human_size(downloaded)}", end="", flush=True)
        print()

        actual_md5 = md5_file(tmp)
        if actual_md5 != expected_md5:
            print(clr(f"    ✗ MD5 invalide ! attendu={expected_md5}  obtenu={actual_md5}", RED))
            tmp.unlink()
            return False

        tmp.rename(dest)
        print(clr("    ✓ OK — MD5 vérifié", GREEN))
        return True

    except urllib.error.URLError as e:
        print(clr(f"\n    ✗ Erreur réseau : {e}", RED))
        if tmp.exists():
            tmp.unlink()
        return False
    except KeyboardInterrupt:
        print(clr("\n    ✗ Interrompu", YELLOW))
        if tmp.exists():
            tmp.unlink()
        raise


def do_download(entries: list, label: str):
    """Télécharge toutes les binaries d'une liste d'entrées."""
    ok, fail = 0, 0
    for entry in entries:
        app_dir = OUTPUT_DIR / safe_name(entry["appName"])
        mode = entry.get("rules", {}).get("mode", "?")
        mc = MODE_COLORS.get(mode, "")
        print(f"\n  {clr(entry['appName'], BOLD)}  v{entry['versionName']}  [{clr(mode, mc)}]")
        for binary in entry.get("binaries", []):
            url = binary["url"]
            dest = app_dir / url.split("/")[-1]
            try:
                if download_file(url, dest, binary.get("checksum", ""), binary.get("size", 0)):
                    ok += 1
                else:
                    fail += 1
            except KeyboardInterrupt:
                print(clr("\n[!] Téléchargement interrompu.", YELLOW))
                sys.exit(1)

    print()
    print(clr("─" * 50, DIM))
    print(clr(f"  ✓ Succès : {ok}", GREEN))
    if fail:
        print(clr(f"  ✗ Échecs : {fail}", RED))
    print(f"  Dossier  : {OUTPUT_DIR}")
    print(clr("─" * 50, DIM))


def menu_app_versions(app_name: str, versions: list):
    """Sous-menu : liste des versions d'une app, choix à télécharger."""
    while True:
        print_header(f"{app_name}  —  {len(versions)} version(s) disponible(s)")
        for i, entry in enumerate(versions, 1):
            mode = entry.get("rules", {}).get("mode", "?")
            mc = MODE_COLORS.get(mode, "")
            size = sum(b.get("size", 0) for b in entry.get("binaries", []))
            date = entry.get("releaseDate", "")
            vc = entry.get("versionCode", "")
            latest_tag = clr(" ◀ LATEST", GREEN) if i == 1 else ""
            print(f"  {clr(str(i), BOLD)}.  v{entry['versionName']:<18}  "
                  f"[{clr(mode, mc):<10}]  {human_size(size):>10}  "
                  f"{clr(date, DIM)}  (code {vc}){latest_tag}")

        print()
        print(f"  {clr('a', BOLD)}. Télécharger toutes les versions")
        print(f"  {clr('r', BOLD)}. Retour")
        print()
        choice = ask("  Choix > ")

        if choice.lower() == "r" or choice == "":
            return
        if choice.lower() == "a":
            do_download(versions, app_name)
            ask("\n  [Entrée pour continuer]")
            return
        if choice.isdigit():
            idx = int(choice) - 1
            if 0 <= idx < len(versions):
                do_download([versions[idx]], app_name)
                ask("\n  [Entrée pour continuer]")
                return
        print(clr("  Choix invalide.", YELLOW))


def menu_main(groups: dict[str, list]):
    """Menu principal : liste des apps."""
    app_names = list(groups.keys())

    while True:
        print_header("SFR Connect TV — Téléchargeur d'APKs")
        total_apps = len(app_names)
        for i, name in enumerate(app_names, 1):
            versions = groups[name]
            latest = versions[0]
            mode = latest.get("rules", {}).get("mode", "?")
            mc = MODE_COLORS.get(mode, "")
            nb = len(versions)
            size = sum(b.get("size", 0) for b in latest.get("binaries", []))
            nb_tag = clr(f"({nb} ver.)", DIM) if nb > 1 else clr("(1 ver.) ", DIM)
            print(f"  {clr(str(i), BOLD):>4}.  {name:<32}  "
                  f"v{latest['versionName']:<18}  "
                  f"[{clr(mode, mc):<10}]  {human_size(size):>10}  {nb_tag}")

        print()
        total_size = sum(
            b.get("size", 0)
            for versions in groups.values()
            for entry in [versions[0]]
            for b in entry.get("binaries", [])
        )
        print(clr(f"  {total_apps} applications  |  Taille totale (dernières versions) : {human_size(total_size)}", DIM))
        print()
        print(f"  {clr('t', BOLD)}. Tout télécharger (dernières versions)")
        print(f"  {clr('q', BOLD)}. Quitter")
        print()
        choice = ask("  Numéro d'app ou commande > ")

        if choice.lower() == "q" or choice == "":
            print("\nAu revoir.\n")
            sys.exit(0)

        if choice.lower() == "t":
            latest_all = [v[0] for v in groups.values()]
            total = sum(b.get("size", 0) for e in latest_all for b in e.get("binaries", []))
            print(f"\n  {len(latest_all)} APKs à télécharger — {human_size(total)} au total")
            ans = ask("  Confirmer ? [O/n] ")
            if ans.lower() != "n":
                do_download(latest_all, "Tout")
                ask("\n  [Entrée pour continuer]")
            continue

        if choice.isdigit():
            idx = int(choice) - 1
            if 0 <= idx < total_apps:
                menu_app_versions(app_names[idx], groups[app_names[idx]])
                continue
        print(clr("  Choix invalide.", YELLOW))


def main():
    print(clr("\n[*] Récupération du catalogue CDN SFR...", DIM))
    try:
        catalog = fetch_json(CDN_CONF_URL)
    except Exception as e:
        print(clr(f"[✗] Impossible de récupérer le catalogue : {e}", RED))
        sys.exit(1)

    groups = group_by_app(catalog)
    print(clr(f"[*] {len(catalog)} entrées  |  {len(groups)} applications trouvées", DIM))

    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    menu_main(groups)


if __name__ == "__main__":
    main()

Si ça peut servir à quelqu'un  ;)

SuperBaobab

  • Abonné SFR fibre FttH
  • *
  • Messages: 10
  • Lyon (69)
Installation de Connect TV d'SFR sur box Android TV
« Réponse #146 le: Aujourd'hui à 01:24:51 »
Sinon pour les patchs,
Pour la version 7.3.0 c'est dans smali_classes4/vk/a.smali
Chercher:
.method public final c()Z
    .locals 2

    invoke-virtual {p0}, Lvk/a;->a()Lwk/f;
Et remplacer tout juste qu'à ".end method" par:
.method public final c()Z
    .locals 1

    const/4 v0, 0x1

    return v0
.end method

Pour la version 8.0.0 r1 c'est dans smali_classes4/n1/a.smali
Chercher:
.method public final c()Z
    .locals 2

    .line 1
    invoke-virtual {p0}, Lnl/a;->a()Lol/f;
Et remplacer tout juste qu'à ".end method" par:
.method public final c()Z
    .locals 1

    const/4 v0, 0x1

    return v0
.end method

rooot

  • Abonné RED by SFR fibre FttH
  • *
  • Messages: 3 079
  • 🔵🔵🔵🔵⚪⚪⚪⚪🔴🔴🔴🔴
Installation de Connect TV d'SFR sur box Android TV
« Réponse #147 le: Aujourd'hui à 11:28:07 »
Merci @SuperBaobab
je me suis permis de compiler ton script Python en exécutable Windows avec "Auto Py 2 exe".