@@ -1672,45 +1672,190 @@ def _select_addon_asset(self, assets, addon_id):
16721672
16731673 return asset
16741674
1675+ def _install_from_source_fallback (self , addon_id ):
1676+ """
1677+ Fallback: Download source code from main branch and extract specific addon folder.
1678+ """
1679+ import requests
1680+ import tempfile
1681+ import zipfile
1682+ import shutil
1683+ from switchcraft .services .addon_service import AddonService
1684+ from pathlib import Path
1685+
1686+ repo = "FaserF/SwitchCraft"
1687+ # Download main branch zip
1688+ source_url = f"https://github.com/{ repo } /archive/refs/heads/main.zip"
1689+ logger .info (f"Fallback: Downloading source from { source_url } ..." )
1690+
1691+ resp = requests .get (source_url , timeout = 30 )
1692+ resp .raise_for_status ()
1693+
1694+ with tempfile .TemporaryDirectory () as temp_dir :
1695+ zip_path = Path (temp_dir ) / "source.zip"
1696+ zip_path .write_bytes (resp .content )
1697+
1698+ extract_dir = Path (temp_dir ) / "extracted"
1699+ with zipfile .ZipFile (zip_path , 'r' ) as zip_ref :
1700+ zip_ref .extractall (extract_dir )
1701+
1702+ # Locate addon folder in source
1703+ # Usually: SwitchCraft-main/src/switchcraft_{addon_id} OR SwitchCraft-main/src/switchcraft/assets/addons/{addon_id}
1704+ # Or just search for manifest.json with matching id?
1705+ # Common structure: SwitchCraft-main/src/switchcraft_{addon_id}
1706+
1707+ root_folder = next (extract_dir .iterdir ()) # SwitchCraft-main
1708+
1709+ # Potential locations
1710+ candidates = [
1711+ root_folder / "src" / f"switchcraft_{ addon_id } " ,
1712+ root_folder / "src" / addon_id ,
1713+ root_folder / "src" / "switchcraft" / "assets" / "addons" / addon_id
1714+ ]
1715+
1716+ addon_src = None
1717+ for cand in candidates :
1718+ if cand .exists () and (cand / "manifest.json" ).exists ():
1719+ addon_src = cand
1720+ break
1721+
1722+ if not addon_src :
1723+ raise Exception (f"Addon source for '{ addon_id } ' not found in main branch." )
1724+
1725+ # Zip the addon folder to simulate a release package
1726+ pkg_zip = Path (temp_dir ) / f"{ addon_id } _source.zip"
1727+ shutil .make_archive (str (pkg_zip .with_suffix ('' )), 'zip' , addon_src )
1728+
1729+ logger .info (f"Installing { addon_id } from repackaged source..." )
1730+ return AddonService ().install_addon (str (pkg_zip ))
1731+
16751732 def _download_and_install_github (self , addon_id ):
16761733 """Helper to download/install without UI code mixed in."""
16771734 import requests
16781735 import tempfile
16791736 from switchcraft .services .addon_service import AddonService
1737+ from switchcraft .utils .config import SwitchCraftConfig
1738+ from switchcraft import __version__
1739+ from pathlib import Path
16801740
16811741 repo = "FaserF/SwitchCraft"
1682- api_url = f"https://api.github.com/repos/{ repo } /releases/latest"
1742+ channel = SwitchCraftConfig .get_update_channel ()
1743+ logger .info (f"Addon download strategy: channel={ channel } , app_version={ __version__ } " )
1744+
1745+ # Strategy Helpers
1746+ def try_matching_version ():
1747+ # Try to find a release tag matching the current version
1748+ # Assuming tags are v<version>
1749+ tag = f"v{ __version__ } "
1750+ api_url = f"https://api.github.com/repos/{ repo } /releases/tags/{ tag } "
1751+ logger .info (f"Trying Matching Version Release: { api_url } " )
1752+ resp = requests .get (api_url , timeout = 10 )
1753+ if resp .status_code == 404 :
1754+ return None
1755+ resp .raise_for_status ()
1756+ return _process_release (resp .json ())
1757+
1758+ def try_latest ():
1759+ api_url = f"https://api.github.com/repos/{ repo } /releases/latest"
1760+ logger .info (f"Trying Latest Release: { api_url } " )
1761+ resp = requests .get (api_url , timeout = 10 )
1762+ if resp .status_code == 404 :
1763+ return None # No latest release
1764+ resp .raise_for_status ()
1765+ return _process_release (resp .json ())
1766+
1767+ def try_newest_release ():
1768+ # Gets list of releases (including pre-releases) and picks first
1769+ api_url = f"https://api.github.com/repos/{ repo } /releases"
1770+ logger .info (f"Trying Newest Release (inc. beta): { api_url } " )
1771+ resp = requests .get (api_url , timeout = 10 )
1772+ resp .raise_for_status ()
1773+ releases = resp .json ()
1774+ if not releases :
1775+ return None
1776+ return _process_release (releases [0 ]) # First is newest
1777+
1778+ def try_source ():
1779+ logger .info ("Trying Source Code Fallback..." )
1780+ return self ._install_from_source_fallback (addon_id )
1781+
1782+ def _process_release (release_data ):
1783+ assets = release_data .get ("assets" , [])
1784+ asset = self ._select_addon_asset (assets , addon_id )
1785+ if not asset :
1786+ return None
1787+
1788+ download_url = asset ["browser_download_url" ]
1789+ asset_name = asset .get ("name" , f"{ addon_id } .zip" )
1790+ logger .info (f"Found { asset_name } in release { release_data .get ('tag_name' )} , downloading from: { download_url } " )
1791+
1792+ with tempfile .TemporaryDirectory () as temp_dir :
1793+ dl_path = Path (temp_dir ) / asset_name
1794+ with requests .get (download_url , stream = True , timeout = 30 ) as r :
1795+ r .raise_for_status ()
1796+ with open (dl_path , 'wb' ) as f :
1797+ for chunk in r .iter_content (chunk_size = 8192 ):
1798+ f .write (chunk )
1799+
1800+ return AddonService ().install_addon (str (dl_path ))
1801+
1802+ # Execution Logic based on Channel
1803+ last_error = None
1804+
1805+ # 1. Stable Channel
1806+ if channel == "stable" :
1807+ # Attempt 1: Stable Latest
1808+ try :
1809+ if res := try_latest (): return res
1810+ except Exception as e :
1811+ logger .warning (f"Stable download failed: { e } " )
1812+ last_error = e
16831813
1684- resp = requests .get (api_url , timeout = 10 )
1685- resp .raise_for_status ()
1814+ # Attempt 2: Beta/Pre-release Fallback
1815+ try :
1816+ if res := try_newest_release (): return res
1817+ except Exception as e :
1818+ logger .warning (f"Beta fallback failed: { e } " )
1819+ last_error = e
16861820
1687- assets = resp .json ().get ("assets" , [])
1688- asset = self ._select_addon_asset (assets , addon_id )
1821+ # Attempt 3: Source Fallback
1822+ try :
1823+ return try_source ()
1824+ except Exception as e :
1825+ logger .error (f"Source fallback failed: { e } " )
1826+ raise e # Raise specific source error if all else fails
16891827
1690- if not asset :
1691- # List available assets for debugging
1692- available_assets = [a ["name" ] for a in assets ]
1693- candidates = [f"switchcraft_{ addon_id } .zip" , f"{ addon_id } .zip" ]
1694- logger .warning (f"Addon { addon_id } not found in latest release. Searched for: { candidates } . Available assets: { available_assets } " )
1695- raise Exception (f"Addon { addon_id } not found in latest release. Searched for: { ', ' .join (candidates )} . Available: { ', ' .join (available_assets [:10 ])} " )
1828+ # 2. Beta Channel
1829+ elif channel == "beta" :
1830+ # Attempt 1: Newest Release (Beta)
1831+ try :
1832+ if res := try_newest_release (): return res
1833+ except Exception as e :
1834+ logger .warning (f"Beta download failed: { e } " )
1835+ last_error = e
16961836
1697- download_url = asset ["browser_download_url" ]
1698- asset_name = asset .get ("name" , f"{ addon_id } .zip" )
1699- logger .info (f"Found { asset_name } in release, downloading from: { download_url } " )
1837+ # Attempt 2: Source Fallback
1838+ try :
1839+ return try_source ()
1840+ except Exception as e :
1841+ raise e
17001842
1701- with tempfile .NamedTemporaryFile (delete = False , suffix = ".zip" ) as tmp :
1702- d_resp = requests .get (download_url , timeout = 30 )
1703- d_resp .raise_for_status ()
1704- tmp .write (d_resp .content )
1705- tmp_path = tmp .name
1843+ # 3. Dev Channel
1844+ else : # dev / nightly
1845+ # Attempt 1: Source (Preferred for Dev)
1846+ try :
1847+ return try_source ()
1848+ except Exception as e :
1849+ logger .warning (f"Source download failed: { e } " )
1850+ last_error = e
17061851
1707- try :
1708- return AddonService ().install_addon (tmp_path )
1709- finally :
1852+ # Attempt 2: Newest Release Fallback
17101853 try :
1711- os . unlink ( tmp_path )
1854+ if res := try_newest_release (): return res
17121855 except Exception as e :
1713- logger .debug (f"Failed to cleanup temp file { tmp_path } : { e } " )
1856+ raise last_error or e
1857+
1858+ raise Exception (f"Addon '{ addon_id } ' could not be found in any channel fallback." )
17141859
17151860 def _download_addon_from_github (self , addon_id ):
17161861 """
0 commit comments