tis-template-microsoft-store-app icon

Template Microsoft Store App

Paquet d’installation silencieuse pour Template Microsoft Store App

0-88
Template
Template

tis-template-microsoft-store-app

Paquet modèle pour créer et maintenir des paquets d'applications Microsoft Store avec WAPT.

Procédure d'installation

Ce paquetage sert de modèle pour créer des paquets WAPT qui récupèrent, versionnent et empaquettent automatiquement les applications disponibles dans le Microsoft Store (UWP / MSIX / APPX).

Procédure d'utilisation

Étape 1 - Télécharger le paquet Téléchargez le paquet tis-template-microsoft-store-app dans votre console WAPT locale

Étape 2 - Lancer le script de mise à jour Ouvrez la console WAPT et faites un clic droit sur le paquet → Lancez update_package. Ce script vous guidera dans la création de votre paquet d'applications Microsoft Store.

Questions interactives (posées par update_package.py)

Au cours du processus de mise à jour, plusieurs boîtes de dialogue apparaîtront pour personnaliser votre paquet : Étape Question / Dialogue Description

-1 Entrée de l'identifiant de l'application : Entrez l'identifiant de l'application du Microsoft Store (ex : https://apps.microsoft.com/store/detail/<APP_ID>)

-2 Sélection du paquet : Une liste des binaires disponibles s'affiche. Sélectionnez la version .appx / .msixbundle appropriée.

-3 Sélection de l'architecture : S'il existe plusieurs architectures (x64, arm64, etc.), sélectionnez celle à inclure.

-4 Sélection de la catégorie : Choisissez une ou plusieurs catégories WAPT (Internet, Office, Media, etc.).

-5 Nom du package : Vous pouvez renommer la valeur control.package (par défaut : générée automatiquement à partir du nom de l'application).

-6 Nom d'affichage : Définissez le nom du contrôle affiché dans le Self-Service.

-7 Description : Vous pouvez modifier ou accepter la description pré-remplie (extraite du Microsoft Store).

Video Explicative

Vous pouvez suivre cette vidéo pour plus d'explications :

  • package: tis-template-microsoft-store-app
  • name: Template Microsoft Store App
  • version: 0-88
  • categories: Template
  • maintainer: WAPT Team,Tranquil IT,Jimmy Pelé
  • licence: wapt_public
  • locale: all
  • target_os: windows
  • architecture: all
  • signature_date:
  • size: 40.31 Ko

package           : tis-template-microsoft-store-app
version           : 0-88
architecture      : all
section           : base
priority          : optional
name              : Template Microsoft Store App
categories        : Template
maintainer        : WAPT Team,Tranquil IT,Jimmy Pelé
description       : Once imported, run the update_package() function to automatically generate your package for a UWP application available on the Microsoft Store
depends           : 
conflicts         : 
maturity          : PROD
locale            : all
target_os         : windows
min_wapt_version  : 2.3
sources           : 
installed_size    : 
impacted_process  : 
description_fr    : Une fois importé, exécutez la fonction update_package() afin de générer automatiquement votre package pour une application UWP disponible sur le Microsoft Store.
description_pl    : Po zaimportowaniu uruchom funkcję update_package(), aby automatycznie wygenerować pakiet dla aplikacji UWP dostępnej w sklepie Microsoft Store
description_de    : Führen Sie nach dem Import die Funktion update_package() aus, um Ihr Paket für eine im Microsoft Store erhältliche UWP-Anwendung automatisch zu generieren
description_es    : Una vez importado, ejecute la función update_package() para generar automáticamente su paquete para una aplicación UWP disponible en Microsoft Store
description_pt    : Uma vez importado, execute a função update_package() para gerar automaticamente o seu pacote para uma aplicação UWP disponível na Microsoft Store
description_it    : Una volta importato, eseguire la funzione update_package() per generare automaticamente il pacchetto per un'applicazione UWP disponibile sul Microsoft Store
description_nl    : Voer na het importeren de functie update_package() uit om automatisch je pakket te genereren voor een UWP-toepassing die beschikbaar is in de Microsoft Store
description_ru    : После импорта запустите функцию update_package() для автоматической генерации пакета для UWP-приложения, доступного в Microsoft Store
audit_schedule    : 
editor            : 
keywords          : 
licence           : wapt_public
homepage          : 
package_uuid      : 73cf4d9f-485a-426e-b1ce-75b277e0ac05
valid_from        : 
valid_until       : 
forced_install_on : 
changelog         : 
min_os_version    : 10.0
max_os_version    : 
icon_sha256sum    : aea6d0c53867b3d774e670da830d8ef922bd93a4ca37ea565e7bbb8465152983
signer            : Tranquil IT
signer_fingerprint: 8c5127a75392be9cc9afd0dbae1222a673072c308c14d88ab246e23832e8c6bb
signature_date    : 2026-03-26T19:00:30.000000
signed_attributes : package,version,architecture,section,priority,name,categories,maintainer,description,depends,conflicts,maturity,locale,target_os,min_wapt_version,sources,installed_size,impacted_process,description_fr,description_pl,description_de,description_es,description_pt,description_it,description_nl,description_ru,audit_schedule,editor,keywords,licence,homepage,package_uuid,valid_from,valid_until,forced_install_on,changelog,min_os_version,max_os_version,icon_sha256sum,signer,signer_fingerprint,signature_date,signed_attributes
signature         : PEmYX2jwn3Z3wGK4nStJXI2pK4Mq0buStmYHb46z+C8I4ApION7FXa9EXC/7EdAl5gCrg1f7Is23Jm+LenPvtkUlUuEzi51OVQRto1p+azQDFCeyn2xgw4BqjTdLWGbJfHm4P6ZMVEwocWYLdNMNqVrA2dIdz5vjRZPflgY+mh1k14pHYaK4WqVIPi4xCXYdmXQKClnOdafaOthUf+e8GHebYRixS2+mjxAJS6ExD8iC6JWcD119KXqo5vjwnjax1jQ4WDBxRhwEH2aBAjnlJ70q/dF2eGUNkP003cfC2QtTLdeaQHEN1/IKGQzld1bz1J4KdNn5mD5Wz9CCvm7stg==

# -*- coding: utf-8 -*-
from setuphelpers import *


appx_package_name = ""
appx_dir = makepath(programfiles, "WindowsAppsInstallers")


def install():
    # Declare local variables
    bins_dir = control.package.split("-", 1)[1]
    old_appx_bins_dir = makepath(appx_dir, bins_dir)
    appx_bins_dir = makepath(os.getcwd(), bins_dir)
    shared_appx_bins_dir = makepath(appx_dir, bins_dir)

    # Removing sources because they are no longer needed
    if isdir(old_appx_bins_dir):
        print("Removing: %s" % (old_appx_bins_dir))
        remove_tree(old_appx_bins_dir)
    if isdir(appx_dir) and dir_is_empty(appx_dir):
        print("Removing: %s since it is empty" % (appx_dir))
        remove_tree(appx_dir)

    # Removing binaries of different architectures
    for f in glob.glob(appx_bins_dir + "/*"):
        fname = f.split(os.sep)[-1]
        if control.architecture == "all":
            if "x86" in glob.glob(makepath(appx_bins_dir, f"{appx_package_name}_*"))[0].split(os.sep)[-1]:
                if "x86" not in fname and "neutral" not in fname:
                    remove_file(f)
            else:
                if get_host_architecture() not in fname and "neutral" not in fname:
                    remove_file(f)
        else:
            if get_host_architecture() not in fname and "neutral" not in fname:
                remove_file(f)

    bin_path = glob.glob(makepath(appx_bins_dir, f"{appx_package_name}_*"))[0]
    dependencies_paths = [a for a in glob.glob(makepath(appx_bins_dir, "*")) if appx_package_name not in a]

    is_emsixbundle = bin_path.lower().endswith(".emsixbundle")

    # Installing the UWP application if needed
    appxprovisionedpackage = run_powershell(
        f'Get-AppXProvisionedPackage -Online | Where-Object DisplayName -Like "{appx_package_name}"'
    )
    if appxprovisionedpackage is None:
        remove_appx(appx_package_name, False)
        appxprovisionedpackage = {"Version": "0"}
    elif force:
        uninstall()

    if Version(appxprovisionedpackage["Version"], 4) < Version(control.get_software_version(), 4):
        print(f"Preparing installation: {bin_path.split(os.sep)[-1]} ({control.get_software_version()})")
        killalltasks(ensure_list(control.impacted_process))

        # Cas EMSIXBUNDLE :
        # - on copie le bundle dans un emplacement accessible à tous
        # - on installe les dépendances si possible en install()
        # - le bundle principal sera installé dans session_setup()
        if is_emsixbundle:
            if not isdir(shared_appx_bins_dir):
                mkdirs(shared_appx_bins_dir)

            shared_bin_path = makepath(shared_appx_bins_dir, os.path.basename(bin_path))
            print(f"Copying EMSIX bundle to: {shared_bin_path}")
            filecopyto(bin_path, shared_bin_path)

            # Copier aussi les dépendances pour session_setup au cas où elles seraient utiles
            for dep in dependencies_paths:
                dep_target = makepath(shared_appx_bins_dir, os.path.basename(dep))
                print(f"Copying dependency to: {dep_target}")
                filecopyto(dep, dep_target)

            # Installer les dépendances en machine si possible
            for dep in dependencies_paths:
                dep_name = os.path.basename(dep)
                if dep.lower().endswith((".appx", ".msix", ".appxbundle", ".msixbundle")):
                    print(f"Installing dependency: {dep_name}")
                    run_powershell(
                        f'Add-AppxProvisionedPackage -Online -PackagePath "{dep}" -SkipLicense',
                        output_format="text",
                    )
                else:
                    print(f"Skipping dependency (unsupported for install phase): {dep_name}")

        else:
            dependencies_pathes = ",".join([f'"{a}"' for a in dependencies_paths])
            add_appx_cmd = f'Add-AppxProvisionedPackage -Online -PackagePath "{bin_path}" -SkipLicense'
            if dependencies_pathes:
                add_appx_cmd += f" -DependencyPackagePath {dependencies_pathes}"

            print(f"Installing: {bin_path.split(os.sep)[-1]} ({control.get_software_version()})")
            run_powershell(add_appx_cmd, output_format="text")
    else:
        print(f'{appxprovisionedpackage["PackageName"]} is already installed and up-to-date.')


def session_setup():
    bins_dir = control.package.split("-", 1)[1]
    shared_appx_bins_dir = makepath(appx_dir, bins_dir)

    candidates = glob.glob(makepath(shared_appx_bins_dir, f"{appx_package_name}_*"))
    if not candidates:
        print(f"No session package found for {appx_package_name} in {shared_appx_bins_dir}")
        return

    bin_path = candidates[0]

    if bin_path.lower().endswith(".emsixbundle"):
        print(f"Installing EMSIX bundle in session: {bin_path}")
        run_powershell(f'Add-AppPackage "{bin_path}"', output_format="text")


def uninstall():
    print(f"Removing AppX: {appx_package_name}")
    remove_appx(appx_package_name)

    bins_dir = control.package.split("-", 1)[1]
    shared_appx_bins_dir = makepath(appx_dir, bins_dir)
    if isdir(shared_appx_bins_dir):
        print(f"Removing cached installer directory: {shared_appx_bins_dir}")
        remove_tree(shared_appx_bins_dir)

    if isdir(appx_dir) and dir_is_empty(appx_dir):
        print("Removing: %s since it is empty" % appx_dir)
        remove_tree(appx_dir)


def audit():
    # Declaring local variables
    audit_result = "OK"
    audit_version = True
    appxprovisionedpackage = run_powershell(
        f'Get-AppXProvisionedPackage -Online | Where-Object DisplayName -Like "{appx_package_name}"'
    )

    # Auditing software
    if appxprovisionedpackage is None:
        print(f"{appx_package_name} is not installed.")
        audit_result = "ERROR"
    elif audit_version:
        if Version(appxprovisionedpackage.get("Version", "0"), 4) < Version(control.get_software_version(), 4):
            print(
                f'{appxprovisionedpackage["PackageName"]} is installed in version: {appxprovisionedpackage["Version"]} instead of: {control.get_software_version()}.'
            )
            audit_result = "WARNING"
        else:
            print(f'{appxprovisionedpackage["PackageName"]} is installed and up-to-date.')
    else:
        print(f'{appxprovisionedpackage["PackageName"]} is installed.')

    return audit_result


def remove_appx(package, default_user=True):
    """Remove Windows AppX package from the computer environment, excluding NonRemovable packages.

    Args:
        package (str): AppX package name. You can use an asterisk (*) as a wildcard.
        default_user (bool): Remove AppX package from the Windows image to prevent installation for new users.

    .. versionadded:: 2.2

    .. versionchanged:: 2.5
        No longer try to remove NonRemovable AppX package
    """
    if running_as_admin() or running_as_system():
        if default_user:
            run_powershell(
                f'Get-AppXProvisionedPackage -Online | Where-Object DisplayName -Like "{package}" | Remove-AppxProvisionedPackage -Online -AllUsers',
                output_format="text",
            )
        run_powershell(
            r'Get-AppxPackage -Name "%s" -AllUsers | Where-Object {{ -not ($_.NonRemovable) }} | Remove-AppxPackage -AllUsers' % package,
            output_format="text",
        )
    else:
        run_powershell(
            r'Get-AppxPackage -Name "%s" | Where-Object {{ -not ($_.NonRemovable) }} | Remove-AppxPackage' % package,
            output_format="text",
        )

def session_cleanup():
    print(f"Removing AppX from session: {appx_package_name}")
    remove_appx(appx_package_name, default_user=False)

# -*- coding: utf-8 -*-
from setuphelpers import *

try:
    from setupdevhelpers import *
except:
    pass

import json
import waptguihelper
import html
from xml.dom import minidom
import requests
import re
from datetime import datetime

release_name_map = {"retail": "Retail", "RP": "Release Preview", "WIS": "Insider Slow", "WIF": "Insider Fast"}
release_type = "retail"
release_name = release_name_map[release_type]
arch = "x64"

# For debug mode, set as True
debug_mode = False

# To make 5 api requests, set as True / Only use if retrieve binary is bad
loop_enable = False


download_all_dependencies = True

REQUEST_TIMEOUT = 60


def http_get(url, proxies=None, verify=True, **kwargs):
    return requests.get(
        url,
        proxies=proxies,
        verify=verify,
        timeout=REQUEST_TIMEOUT,
        **kwargs,
    )


def http_post(url, data=None, headers=None, proxies=None, verify=True, **kwargs):
    return requests.post(
        url,
        data=data,
        headers=headers,
        proxies=proxies,
        verify=verify,
        timeout=REQUEST_TIMEOUT,
        **kwargs,
    )


def parse_iso_datetime(iso_str):
    if not iso_str:
        return datetime.min

    try:
        value = iso_str.strip()
        if value.endswith("Z"):
            value = value[:-1] + "+00:00"

        # Normalize too many fractional second digits if present
        m = re.match(r"(.+\.\d{6})\d+(.*)$", value)
        if m:
            value = m.group(1) + m.group(2)

        return datetime.fromisoformat(value)
    except Exception:
        return datetime.min


def get_setup_appx_package_name():
    if not isfile("setup.py"):
        return None

    with open("setup.py", "r", encoding="utf-8") as f:
        for line in f.readlines():
            m = re.match(r'\s*appx_package_name\s*=\s*["\']([^"\']+)["\']', line)
            if m:
                return m.group(1)

    return None


def set_setup_appx_package_name(appx_package_name):
    if not isfile("setup.py"):
        return

    new_lines = []
    found = False

    with open("setup.py", "r", encoding="utf-8") as f:
        for line in f.readlines():
            if re.match(r'\s*appx_package_name\s*=', line):
                line = 'appx_package_name = "%s"\n' % appx_package_name
                found = True
            new_lines.append(line)

    if not found:
        new_lines.append('\nappx_package_name = "%s"\n' % appx_package_name)

    with open("setup.py", "w", encoding="utf-8") as f:
        f.writelines(new_lines)


def get_target_arch():
    archs = ensure_list(control.architecture)

    if "x64" in archs:
        return "x64"
    if "x86" in archs:
        return "x86"
    if "arm64" in archs:
        return "arm64"
    if "arm" in archs:
        return "arm"
    if "all" in archs:
        return "all"

    return os_arc()


def get_arch_score(file_arch, wanted_arch):
    if wanted_arch == "all":
        return 2
    if file_arch == wanted_arch:
        return 3
    if file_arch in ("all", "neutral"):
        return 2
    return 0


def get_type_score(filename):
    ext = os.path.splitext(filename.lower())[1]
    ext_priority = {
        ".msixbundle": 100,
        ".appxbundle": 90,
        ".msix": 80,
        ".appx": 70,
        ".emsixbundle": 60,
        ".eappxbundle": 50,
        ".emsix": 40,
        ".eappx": 30,
    }
    return ext_priority.get(ext, 0)


def update_package():
    package_updated = False
    proxies = get_proxies()

    if not proxies:
        proxies = get_proxies_from_wapt_console()

    store_url = control.sources

    if not store_url:
        store_id = waptguihelper.input_dialog(
            "Choice of app",
            "Enter the windows store app id (foundable in package url: https://apps.microsoft.com/store/apps)",
            store_url,
        ).split("/")[-1]
    else:
        store_id = store_url.split("/")[-1]
    store_id = store_id.split("?")[0]

    url_ms = "https://apps.microsoft.com/store/detail/%s" % store_id
    control.sources = url_ms
    control.save_control_to_wapt()

    package_json = get_json(store_id, proxies=proxies)
    package_json_fr = get_json_fr(store_id, proxies=proxies)

    if isfile("package.json"):
        package_infos = json_load_file("package.json")
    else:
        package_infos = {
            "version_prefix": None,
            "ignored_versions": [],
            "package_name_filter": None,
        }

    ignored_versions = package_infos.get("ignored_versions", [])
    version_prefix = package_infos.get("version_prefix", None)
    package_name_filter = package_infos.get("package_name_filter", None)

    setup_appx_package_name = get_setup_appx_package_name()
    if not package_name_filter and setup_appx_package_name:
        package_name_filter = setup_appx_package_name

    if debug_mode:
        print("\n\n=== CAREFUL : DEBUG MODE IS ACTIVE ===\n")
        print(f"package_name_filter : {package_name_filter}")
        print(f"setup_appx_package_name : {setup_appx_package_name}")
        print(f"version_prefix : {version_prefix}")
        print(f"ignored_versions : {ignored_versions}\n")

    print("Retrieving informations from Microsoft Store API, please wait\n")
    all_files_dict = get_all_files(url_ms, proxies)

    if debug_mode:
        all_files_dict_json = json.dumps(all_files_dict, indent=4)
        print("*** Pretty return from get_all_files() ***")
        print(f"all_files_dict : {all_files_dict_json}\n")

    bin_selected_dict = ask_app_filename(
        all_files_dict,
        package_name_filter=package_name_filter,
        ignored_versions=ignored_versions,
        version_prefix=version_prefix,
    )

    if not bin_selected_dict:
        error("No matching package found from API response")

    if debug_mode:
        bin_selected_dict_json = json.dumps(bin_selected_dict, indent=4)
        print("*** Selected file from ask_app_filename() ***")
        print(f"bin_selected_dict : {bin_selected_dict_json}\n")

    selected_package_name = bin_selected_dict[0]["bin_name"].split("_")[0]
    set_setup_appx_package_name(selected_package_name)

    bin_selected = bin_selected_dict[0]
    latest_bin = bin_selected["bin_name"]
    version = bin_selected["version"]
    download_url = bin_selected["download_url"]
    package_name = bin_selected["package_name"]
    software_name = bin_selected["software_name"]
    package_arch = bin_selected["package_arch"]

    if bin_selected.get("is_encrypted"):
        print("Selected encrypted package: %s" % latest_bin)

    if download_url.split("/")[2].endswith("microsoft.com"):
        if not isfile(latest_bin):
            print("Downloading: %s" % latest_bin)
            wget(download_url, latest_bin, proxies=proxies)
        else:
            print("Binary file version corresponds to online version")
    else:
        error("ERROR: The retrieved url will not download from microsoft's servers")

    if not params.get("running_as_luti"):
        if package_arch == "all":
            ask_control_architecture(package_arch)
        ask_control_categories()
        ask_control_package(package_name, "template-microsoft-store", remove_base_files=True)
        ask_control_name(software_name, "Template Microsoft Store")
        ask_control_description("update_package", get_description(package_json))
        ask_control_description_fr("update_package", get_description_fr(package_json_fr))

    uwp_app_dict = make_dependency_dict(selected_package_name, all_files_dict)

    if not uwp_app_dict:
        error(f'"{selected_package_name}" not found in API file list. This is probably caused by package.json filters:\n{package_infos}')

    newer_uwp_app = next(iter(uwp_app_dict.values()))

    downloaded_dependencies = download_api_dependencies(bin_selected, all_files_dict, proxies)

    if Version(version) > Version(control.get_software_version()):
        print("Software version updated (from: %s to: %s)" % (control.get_software_version(), Version(version)))
        package_updated = True
    else:
        print("Software version up-to-date (%s)" % Version(version))

    if debug_mode:
        print("Deleting binary files and exiting")
        for uwp_file in glob.glob(f'{newer_uwp_app["Name"]}*{newer_uwp_app["Version"]}*.*'):
            remove_file(uwp_file)
        for dep in downloaded_dependencies:
            if isfile(dep["bin_name"]):
                remove_file(dep["bin_name"])
        return

    control.set_software_version(version)
    control.save_control_to_wapt()

    for f in glob.glob("*.part"):
        remove_file(f)

    bins_dir = control.package.split("-", 1)[1]
    if isdir(bins_dir):
        remove_tree(bins_dir)
    mkdirs(bins_dir)

    for uwp_file in glob.glob(f'{newer_uwp_app["Name"]}*{newer_uwp_app["Version"]}*.*'):
        if isfile(uwp_file) and not isfile(makepath(bins_dir, uwp_file)):
            shutil.move(uwp_file, bins_dir)

    for dep in downloaded_dependencies:
        dep_file = dep["bin_name"]
        if isfile(dep_file) and not isfile(makepath(bins_dir, dep_file)):
            shutil.move(dep_file, bins_dir)

    remove_outdated_binaries(
        version,
        ["appxbundle", "msixbundle", "appx", "msix", "eappx", "emsix", "eappxbundle", "emsixbundle", "part"],
        latest_bin,
    )

    return package_updated


def get_json(app_id, proxies=None):
    url = "https://storeedgefd.dsx.mp.microsoft.com/v9.0/products/%s?market=US&locale=en-us&deviceFamily=Windows.Desktop" % app_id
    data = json.loads(http_get(url, proxies=proxies).text)
    return data


def get_json_fr(app_id, proxies=None):
    url = "https://storeedgefd.dsx.mp.microsoft.com/v9.0/products/%s?market=FR&locale=fr-fr&deviceFamily=Windows.Desktop" % app_id
    data_fr = json.loads(http_get(url, proxies=proxies).text)
    return data_fr


def get_all_files(store_id, proxies):
    all_files, modified_dict, main_file_name = url_generator(
        store_id,
        proxies=proxies
    )

    if debug_mode:
        all_files_json = json.dumps(all_files, indent=4)
        print("*** Raw return from url_generator() ***")
        print(f"all_files : {all_files_json}\n")

    all_files_dict = []

    encrypted_exts = {
        ".eappx",
        ".emsix",
        ".eappxbundle",
        ".emsixbundle",
    }

    for i, download_url in all_files.items():
        bin_name = i.replace("~", "_")
        version_match = re.search(r"_(\d+(?:\.\d+)+)", bin_name)
        version = version_match.group(1) if version_match else "0"

        pkg_splitted = re.split(r"_\d+\.", bin_name)[0]
        package_prefix = control.package.split("-")[0]
        package_name = package_prefix + "-" + pkg_splitted.split("_")[0].replace(".", "-").lower()
        software_name = bin_name.split("_")[0].replace("-", " ").replace(".", " ")

        lower_name = bin_name.lower()
        file_ext = os.path.splitext(lower_name)[1]
        is_encrypted = file_ext in encrypted_exts

        if "arm64" in lower_name:
            package_arch = "arm64"
        elif "arm" in lower_name:
            package_arch = "arm"
        elif "x64" in lower_name:
            package_arch = "x64"
        elif "x86" in lower_name:
            package_arch = "x86"
        elif "neutral" in lower_name:
            package_arch = "all"
        else:
            package_arch = "all"

        file_dict = {
            "version": version,
            "bin_name": bin_name,
            "package_name": package_name,
            "software_name": software_name,
            "package_arch": package_arch,
            "download_url": download_url,
            "file_ext": file_ext,
            "is_encrypted": is_encrypted,
            "modified": modified_dict.get(i),
            "main_file_name": main_file_name,
        }
        all_files_dict.append(file_dict)

    return all_files_dict


def url_generator(url, proxies):
    def uwp_gen(data_list):
        cat_id = data_list["WuCategoryId"]
        main_file_name = data_list["PackageFamilyName"].split("_")[0]
        release_type = "Retail"

        with open(rf"{basedir}\data\xml\GetCookie.xml", "r") as f:
            cookie_content = f.read()

        out = http_post(
            "https://fe3cr.delivery.mp.microsoft.com/ClientWebService/client.asmx",
            data=cookie_content,
            headers={"Content-Type": "application/soap+xml; charset=utf-8"},
            proxies=proxies,
            verify=False,
        ).text
        doc = minidom.parseString(out)

        cookie = doc.getElementsByTagName("EncryptedData")[0].firstChild.nodeValue

        with open(rf"{basedir}\data\xml\WUIDRequest.xml", "r") as f:
            cat_id_content = f.read().format(cookie, cat_id, release_type)

        out = http_post(
            "https://fe3cr.delivery.mp.microsoft.com/ClientWebService/client.asmx",
            data=cat_id_content,
            headers={"Content-Type": "application/soap+xml; charset=utf-8"},
            proxies=proxies,
            verify=False,
        ).text

        doc = minidom.parseString(html.unescape(out))

        if loop_enable:
            for _ in range(4):
                out2 = http_post(
                    "https://fe3cr.delivery.mp.microsoft.com/ClientWebService/client.asmx",
                    data=cat_id_content,
                    headers={"Content-Type": "application/soap+xml; charset=utf-8"},
                    proxies=proxies,
                    verify=False,
                ).text

                doc2 = minidom.parseString(html.unescape(out2))
                imp = doc.importNode(doc2.childNodes[0], True)
                doc.childNodes[0].appendChild(imp)

        filenames = {}
        for node in doc.getElementsByTagName("Files"):
            try:
                filenames[
                    node.parentNode.parentNode.getElementsByTagName("ID")[0].firstChild.nodeValue
                ] = (
                    f"{node.firstChild.attributes['InstallerSpecificIdentifier'].value}_{node.firstChild.attributes['FileName'].value}",
                    node.firstChild.attributes["Modified"].value,
                )
            except (KeyError, AttributeError, IndexError):
                continue

        if not filenames:
            raise Exception("server returned an empty list")

        identities = {}
        modified_dict = {}
        for node in doc.getElementsByTagName("SecuredFragment"):
            try:
                file_name, modified = filenames[
                    node.parentNode.parentNode.parentNode.getElementsByTagName("ID")[0].firstChild.nodeValue
                ]
                update_identity = node.parentNode.parentNode.firstChild
                modified_dict[file_name] = modified
                identities[file_name] = (
                    update_identity.attributes["UpdateID"].value,
                    update_identity.attributes["RevisionNumber"].value,
                )
            except (KeyError, AttributeError, IndexError):
                continue

        api_dict = {value: identities[value] for value in modified_dict}

        if debug_mode:
            api_dict_json = json.dumps(api_dict, indent=4)
            print("*** First API Return from url_generator() ***")
            print(f"api_dict : {api_dict_json}\n")

        with open(rf"{basedir}\data\xml\FE3FileUrl.xml", "r") as f:
            file_content = f.read()

        file_dict = {}

        for file_name, (updateid, revisionnumber) in api_dict.items():
            out = http_post(
                "https://fe3cr.delivery.mp.microsoft.com/ClientWebService/client.asmx/secured",
                data=file_content.format(updateid, revisionnumber, release_type),
                headers={"Content-Type": "application/soap+xml; charset=utf-8"},
                proxies=proxies,
                verify=False,
            ).text
            doc = minidom.parseString(out)
            for i in doc.getElementsByTagName("FileLocation"):
                url_item = i.getElementsByTagName("Url")[0].firstChild.nodeValue
                if len(url_item) != 99:
                    file_dict[file_name] = url_item

        if len(file_dict) != len(api_dict):
            raise Exception("server returned an incomplete list")

        return file_dict, modified_dict, main_file_name

    def non_uwp_gen(product_id):
        api = f"https://storeedgefd.dsx.mp.microsoft.com/v9.0/packageManifests//{product_id}?market=US&locale=en-us&deviceFamily=Windows.Desktop"

        data = http_get(api, proxies=proxies).text
        datas = json.loads(data)

        if not datas.get("Data"):
            raise Exception("server returned an empty list")

        file_name = datas["Data"]["Versions"][0]["DefaultLocale"]["PackageName"]
        installer_list = datas["Data"]["Versions"][0]["Installers"]

        download_data = set((d["Architecture"], d["InstallerLocale"], d["InstallerType"], d["InstallerUrl"]) for d in installer_list)
        curr_arch = os_arc()
        download_data = list(download_data)

        arch_item, locale, installer_type, url_item = download_data[0]
        if len(download_data) > 1:
            for data_item in download_data[1:]:
                if arch_item not in ("neutral", curr_arch) and data_item[0] in ("neutral", curr_arch):
                    arch_item, locale, installer_type, url_item = data_item
                elif data_item[0] == arch_item and data_item[1] != locale and ("us" in data_item[1] or "en" in data_item[1]):
                    locale, installer_type, url_item = data_item[1], data_item[2], data_item[3]
                    break

        main_file_name = clean_name(file_name) + "." + installer_type
        file_dict = {main_file_name: url_item}
        return file_dict, {}, main_file_name

    def clean_name(badname):
        name = "".join([(i if (64 < ord(i) < 91 or 96 < ord(i) < 123) else "") for i in badname])
        return name.lower()

    pattern = re.compile(r".+\/([^\/\?]+)(?:\?|$)")
    matches = pattern.search(str(url))
    if not matches:
        raise Exception("No Data Found: --> [You Selected Wrong Page, Try Again!]")

    product_id = matches.group(1)
    details_api = f"https://storeedgefd.dsx.mp.microsoft.com/v9.0/products/{product_id}?market=US&locale=en-us&deviceFamily=Windows.Desktop"
    data = http_get(details_api, proxies=proxies).text
    response = json.loads(
        data,
        object_hook=lambda obj: {k: json.loads(v) if k == "FulfillmentData" else v for k, v in obj.items()}
    )

    if not response.get("Payload"):
        raise Exception("No Data Found: --> [You Selected Wrong Page, Try Again!]")

    response_data = response["Payload"]["Skus"][0]
    data_list = response_data.get("FulfillmentData")

    if data_list:
        return uwp_gen(data_list)
    else:
        return non_uwp_gen(product_id)


def ask_app_filename(all_files_dict, package_name_filter=None, ignored_versions=None, version_prefix=None):
    if ignored_versions is None:
        ignored_versions = []

    selected = [
        a for a in all_files_dict
        if (
            a["package_arch"] in ensure_list(control.architecture)
            or control.architecture == "all"
            or a["package_arch"] == "all"
        )
    ]

    candidates = []
    for app_file in selected:
        if package_name_filter:
            if not app_file["bin_name"].startswith(f"{package_name_filter}_"):
                continue

        if version_prefix is not None:
            n = len(version_prefix.split("."))
            if Version(app_file["version"], n) != Version(version_prefix, n):
                continue

        if any(
            Version(app_file["version"], len(v.split("."))) == Version(v, len(v.split(".")))
            for v in ignored_versions
        ):
            continue

        candidates.append(app_file)

    if not candidates:
        candidates = selected

    if not candidates:
        return []

    grouped = {}
    for item in candidates:
        pkg_name = item["bin_name"].split("_")[0]
        grouped.setdefault(pkg_name, []).append(item)

    target_arch = get_target_arch()

    if len(grouped) == 1:
        best = select_best_match(candidates, wanted_arch=target_arch)
        return [best] if best else []

    if "template-microsoft-store" in control.package:
        return waptguihelper.grid_dialog(
            "Please select the proper file",
            json.dumps(candidates),
            waptguihelper.GRT_SELECTED,
            '{"columns":[{"propertyname":"version","datatype":"String","required":false,"readonly":false,"width":130},{"propertyname":"bin_name","datatype":"String","required":false,"readonly":false,"width":420},{"propertyname":"package_name","datatype":"String","required":false,"readonly":false,"width":190},{"propertyname":"software_name","datatype":"String","required":false,"readonly":false,"width":172},{"propertyname":"package_arch","datatype":"String","required":false,"readonly":false,"width":88},{"propertyname":"download_url","datatype":"String","required":false,"readonly":false,"width":1472}]}',
        )

    best = select_best_match(candidates, wanted_arch=target_arch)
    return [best] if best else []


def ask_control_categories():
    if control.categories == "Template":
        categories = waptguihelper.grid_dialog(
            "Select package categories",
            [
                "Internet",
                "Utilities",
                "Messaging",
                "Security",
                "System and network",
                "Media",
                "Development",
                "Office",
                "Drivers",
                "Education",
                "Configuration",
                "CAD",
                "Template",
                "Dependencies",
                "Extensions",
            ],
            waptguihelper.GRT_SELECTED,
        )
        if categories:
            control.categories = ",".join([a["unknown"] for a in categories])
        else:
            control.categories = ""
        control.save_control_to_wapt()
    return control.categories


def ask_control_architecture(package_arch):
    if control.categories == "Template":
        architecture = waptguihelper.grid_dialog(
            f"Package Architecture available : {package_arch}",
            [
                "all",
                "x64",
                "x86",
                "arm",
                "arm64",
            ],
            waptguihelper.GRT_SELECTED,
        )
        if architecture:
            control.architecture = ",".join([a["unknown"] for a in architecture])
        else:
            control.architecture = package_arch
        control.save_control_to_wapt()
    return control.architecture


def ask_control_package(control_package, conditionnal_package_name=None, remove_base_files=False):
    if conditionnal_package_name is None or conditionnal_package_name in control.package:
        control.package = waptguihelper.input_dialog(control.package, "You can redefine the package name", control_package)
        control.save_control_to_wapt()

    if conditionnal_package_name in control.package and remove_base_files:
        if isfile("WAPT\\changelog.txt"):
            remove_file("WAPT\\changelog.txt")
        if isfile("WAPT\\icon.png"):
            remove_file("WAPT\\icon.png")
    return control.package


def ask_control_name(control_name, conditionnal_package_name=None):
    if conditionnal_package_name is None or conditionnal_package_name in control.name:
        control.name = waptguihelper.input_dialog(control.name, "You can redefine the name for the self-service", control_name)
        control.save_control_to_wapt()
    return control.name


def ask_control_description(blank_str=None, description_from_store=""):
    control.description = ask_dialog("Description (en)", "Please fill the description (english)", description_from_store)
    control.save_control_to_wapt()
    return control.description


def ask_control_description_fr(blank_str=None, description_from_store=""):
    control.description_fr = ask_dialog("Description (fr)", "Merci de remplir la description (français)", description_from_store)
    control.description_pl = ""
    control.description_de = ""
    control.description_es = ""
    control.description_pt = ""
    control.description_it = ""
    control.description_nl = ""
    control.description_ru = ""
    control.save_control_to_wapt()
    return control.description_fr


def ask_dialog(title, text, default="", stay_on_top=False):
    return waptguihelper.input_dialog(title, text, default, stay_on_top)


def get_description(data):
    return data["Payload"]["Skus"][0]["Description"].replace("\r\n", "").replace("\n", "").replace("\r", "")


def get_description_fr(data_fr):
    return data_fr["Payload"]["Skus"][0]["Description"].replace("\r\n", "").replace("\n", "").replace("\r", "")


def get_package_ext_priority(filename):
    ext_priority = {
        ".msixbundle": 100,
        ".appxbundle": 90,
        ".msix": 80,
        ".appx": 70,
        ".emsixbundle": 60,
        ".eappxbundle": 50,
        ".emsix": 40,
        ".eappx": 30,
    }
    return ext_priority.get(os.path.splitext(filename.lower())[1], 0)


def select_best_match(files_list, wanted_arch=None):
    if not files_list:
        return None

    if wanted_arch is None:
        wanted_arch = get_target_arch()

    compatible = []
    for item in files_list:
        if wanted_arch == "all" or item["package_arch"] in (wanted_arch, "all"):
            compatible.append(item)

    if not compatible:
        compatible = files_list

    def score(x):
        return (
            get_arch_score(x["package_arch"], wanted_arch),
            get_type_score(x["bin_name"]),
            parse_iso_datetime(x.get("modified")),
            Version(x["version"]),
        )

    return max(compatible, key=score)


def is_framework_package(item):
    framework_prefixes = (
        "Microsoft.VCLibs",
        "Microsoft.UI.Xaml",
        "Microsoft.NET.Native.Framework",
        "Microsoft.NET.Native.Runtime",
        "Microsoft.Services.Store.Engagement",
        "Microsoft.WindowsAppRuntime",
    )
    return item["bin_name"].startswith(framework_prefixes)


def same_or_compatible_arch(main_arch, dep_arch):
    if main_arch == "all":
        return True
    if dep_arch == "all":
        return True
    return dep_arch == main_arch


def make_dependency_dict(selected_package_name, all_files_dict):
    candidates = [
        f for f in all_files_dict
        if f["bin_name"].startswith(f"{selected_package_name}_")
    ]

    best = select_best_match(candidates, wanted_arch=get_target_arch())
    if not best:
        return {}

    return {
        best["bin_name"]: {
            "FileName": best["bin_name"],
            "Name": best["bin_name"].split("_")[0],
            "Version": best["version"],
            "Architecture": best["package_arch"],
            "Dependencies": None,
        }
    }


def get_newer_uwp_depency(package_name, all_files_dict, package_arch=None, version_prefix=None, min_version=None):
    candidates = []

    for uwp_app in all_files_dict:
        if not uwp_app["bin_name"].startswith(f"{package_name}_"):
            continue

        if package_arch is not None and not same_or_compatible_arch(package_arch, uwp_app["package_arch"]):
            continue

        if version_prefix is not None:
            n = len(version_prefix.split("."))
            if Version(uwp_app["version"], n) != Version(version_prefix, n):
                continue

        if min_version is not None and Version(uwp_app["version"]) < Version(min_version):
            continue

        candidates.append(uwp_app)

    return select_best_match(candidates, wanted_arch=package_arch or get_target_arch())


def resolve_framework_dependencies(main_package, all_files_dict):
    main_arch = main_package["package_arch"]

    framework_groups = {
        "Microsoft.VCLibs": [],
        "Microsoft.UI.Xaml": [],
        "Microsoft.NET.Native.Framework": [],
        "Microsoft.NET.Native.Runtime": [],
        "Microsoft.Services.Store.Engagement": [],
        "Microsoft.WindowsAppRuntime": [],
    }

    for item in all_files_dict:
        if not is_framework_package(item):
            continue

        if not same_or_compatible_arch(main_arch, item["package_arch"]):
            continue

        for prefix in framework_groups:
            if item["bin_name"].startswith(prefix + "_"):
                framework_groups[prefix].append(item)
                break

    resolved = []
    for _, matches in framework_groups.items():
        best = select_best_match(matches, wanted_arch=main_arch)
        if best:
            resolved.append(best)

    return resolved


def resolve_all_dependencies(main_package, all_files_dict):
    main_name = main_package["bin_name"].split("_")[0]
    main_arch = main_package["package_arch"]

    grouped = {}
    for item in all_files_dict:
        pkg_name = item["bin_name"].split("_")[0]

        if pkg_name == main_name:
            continue

        if not same_or_compatible_arch(main_arch, item["package_arch"]):
            continue

        grouped.setdefault(pkg_name, []).append(item)

    resolved = []
    for _, matches in grouped.items():
        best = select_best_match(matches, wanted_arch=main_arch)
        if best:
            resolved.append(best)

    return resolved


def resolve_dependencies(main_package, all_files_dict, all_dependencies=False):
    if all_dependencies:
        return resolve_all_dependencies(main_package, all_files_dict)
    return resolve_framework_dependencies(main_package, all_files_dict)


def download_api_dependencies(main_package, all_files_dict, proxies):
    downloaded_dependencies = []
    dependencies = resolve_dependencies(
        main_package,
        all_files_dict,
        all_dependencies=download_all_dependencies,
    )

    if dependencies and debug_mode:
        print("\nResolved API dependencies:")
        for dep in dependencies:
            print(f' - {dep["bin_name"]}')
        print("")

    for dep in dependencies:
        latest_bin = dep["bin_name"]
        download_url = dep["download_url"]

        if download_url.split("/")[2].endswith("microsoft.com"):
            if not isfile(latest_bin):
                print("Downloading dependency: %s" % latest_bin)
                wget(download_url, latest_bin, proxies=proxies)
            else:
                print("Dependency already present: %s" % latest_bin)
            downloaded_dependencies.append(dep)
        else:
            error("ERROR: The retrieved URL will not download from microsoft's servers")

    return downloaded_dependencies


def get_uwp_filename_arch(appx_filename, appx_package_name=None):
    if not appx_package_name:
        appx_package_name = None
    if len(glob.glob(f'{appx_package_name}*{appx_filename.split("_")[1]}*')) > 1:
        pass
    elif "arm64" in appx_filename:
        return "arm64"
    elif "arm" in appx_filename:
        return "arm"
    elif "x64" in appx_filename:
        return "x64"
    if appx_filename is not None and "x86" in appx_filename and (appx_package_name is None or appx_package_name not in appx_filename):
        return "x86"

    return "all"


def os_arc():
    machine = control.architecture
    if machine.endswith("arm64"):
        return "arm64"
    if machine.endswith("64"):
        return "x64"
    if machine.endswith("32") or machine.endswith("86"):
        return "x86"
    else:
        return "arm"

b3100f592fa1a3b867363176529f2df82d51bbcc9221dfc95a54f1392a58cf9d : WAPT/README.md
deb8ca89091812a8663d6e901430325f9de1d067e1e1d4f92ac0522e585f9ce9 : WAPT/README_fr.md
38d056ab130f7bf7c481c12636a4e9959de36561d3dfcbe54c6e3571bc0c1dc3 : WAPT/certificate.crt
3a6df07709510e8084cd91c49cc9f6b9ea7a96856e3625465fe4a64da84f5814 : WAPT/changelog.txt
19b70467c3a2cecc663a991738a1cb4354d6b4a68a346a4fb0c26bc398f24c9b : WAPT/control
aea6d0c53867b3d774e670da830d8ef922bd93a4ca37ea565e7bbb8465152983 : WAPT/icon.png
87370426807b83b7916c4f43942cd9bf71b1eeaf3b44728d241efe107927e3a2 : data/xml/FE3FileUrl.xml
2d3dd983c9d83c2464afaa85ab49ad5c78a537a131ec61e0c32f6b446bed4f55 : data/xml/GetCookie.xml
f8a4681fbeafb4ddcaac37b406374143900d8885b59ba7a7a0ec782d79bacd9b : data/xml/WUIDRequest.xml
14d227a2bfacb5437e7304c4522903b46de4e21687c7105dc633c4a5f36c92f5 : luti.json
bd43e5749147a75dbd658cac066506657073da0ee363e93418ed66427dae1bb6 : package.json
b012c7506e1aae774e030f73a8b09bf3a8798b33b1307a51c2464c843f73197b : setup.py
c802d056a635d7c09e516c827ce4f49551b98844183adf0aa5896ea5bdfd2a03 : setupdevhelpers.py
430b03a6bdd562e9c5ff06ba4aec2ac054cf2c9e01ed3cb6e3575bd675d5d1fd : update_package.py
364e01205533a0c6907fb9e8a8f70353985ae81ead35b71aade9a4253184f847 : xmltodict.py

0-74
==
Add possibilities to make multiple requests to get good version if some mirrors are bad
Improve test if target version specified

0-73
==
Add debug_mode to print informations

0-71
==
Rework to use Microsoft API

0-68
===
Add compatibility with .msixbundle not seen as zipfile

0-64
===
if the package is not found in the uwp_app_dict an error is returned.
avoid crash with Add-AppxProvisionedPackage when AppxPackage only is already up-to-date.


0-62
===
"tis-template-microsoft-store-app": changed installation method from "Add-AppxPackage" to "Add-AppxProvisionedPackage". Whole setup.py have been reworked.
"tis-template-microsoft-store-dependency": is now used to install Dependencies/DLC/Add-on and stay based on "Add-AppxPackage" installation method.
now autocomplete control.impacted_process and control.installed_size
fix neutral Architecture
fix some crash in control informations asked once
proxy workarounds
fix control.name fetched from page


0-52
===
improve compatibility with Windows 8.x apps
improve complete_control_*() functions
includes xmltodict instead of running "waptpython -m pip download xmltodict"
now avoiding downloads loop and avoid keeping sources of other apps
check version before checking files in session_setup
implement version_prefix to select a preferred version
architecture is:
    - no longer forced on update
    - pre-populate multi-architectures when relevant
    - only compatible architecture dependencies are now downloaded


0-47
===
now, you need to select the app version that you want to package, which enables you to:
    - Download only useful files without editing the update_package.py file.
    - Create the package for both Windows 11 and Windows 10 without editing the control file.
    - Automatically define ignored_versions saved in the package.json file.
handle multi-architecture packages (only works on min_wapt_version: 2.4).
min_wapt_version: 2.3
adding remove_sources feature but do not using it by default since it may prevent multi-user installations.
now asking if user wanna change control.max_os_version instead of only printing it
full audit
by default control_max_os_version if not filled (safer)


0-43
===
fix get_newer_uwp_app and adding multiple options : (version_prefix, min_version, max_version)
create set_control_os_version_uwp_app function
now only download latest necessary dependencies
fix packages where control.architecture != "all"
smarter descriptions editing
min_wapt_version  : 2.2.3
should now works for Windows 8.1+ apps
fix update_package when files without extensions have been detected
fix update_package when no dependencies are required
to avoid unnecessary downloads you can now use download_app_files(ignored_versions=["2017", "2018", "2019", "2020", "2020.1"])
better control completion
setupdevhelpers is now shipped with advanced functions to reduce code duplication
only complete_control once


0-32
===
now includes dependencies, so the tis-template-microsoft-store-dependency part is no longer needed
auto update icon.png
auto update control.min_os_version
smarter popup to ask for appx_package_name
handle "Windows.Desktop" and "Windows.Universal" TargetDeviceFamily
removes encrypted uwp apps
auto update control.max_os_version if relevant ; max_os_version fiabilized ; Ignore end-of-life Windows versions by default


0-25
===
fix rare case of incorrect glob.glob
better handling of binary suppression


0-24
===
fix silent mode
more information shown in grid_dialog
update_package() was inccorect for some architecture
prefix was forced to tis
now using full AppxPackage Name for dependencies
better handling of remove_outdated_binaries and add_depends
improve used regex
using functions for some popups, functions return associated str
more default actions on popups
fix update_package for 2.3 with setupdevhelpers import
fix package name change, and depencies warning not to block luti build
warn the end-user that dependencies are required and providing instructions on how to acquire and deploy them


0-12
===
asking for categories
asking for description
adding dependencies
changing setup.py appx_package variable
smart update_package() it do not popup anything to download updates
using Microsoft_Store_Fluent_Design_icon
remove_outdated_binaries handle extensions
removing template files when used
allow_remove option
specify arch
min_wapt_version  : 2.2
ask to change package_name
fix re.split
fix multi download on neutral apps
only ask once for description