Sam Mesaj tarihi: Ekim 21, 2023 Mesaj tarihi: Ekim 21, 2023 samsung tv yakında steam link app'i devreden çıkaracağı için alternatif arayışına girmiştim, benzer durum yaşayan varsa bulduğum en ucuz (çok pratik olmasa da) çözümü paylaşmak istedim. https://www.mi.com/us/product/xiaomi-tv-stick-4k/ aldım, şu an 1500₺'den daha ucuza bulabilirsiniz. alternatifleri olan tv stick ve box'lar var, hatta decoding performansları da gördüğüm yorumlara göre daha iyi olabilir ama en ucuzu bu. hevc 4k 60fps decoding'de kabaca 10ms kadar bir decode time var gördüğüm. 5ms olan aletler var denmiş, mesela amazon fire stick 4k ama onda sideloading gerekiyordu sanırım bazı app'ler için düz android tv os olmadığından. neyse ben bunu anlatayım, ios ve android'de mevcut moonlight. telefon ve tabletinizden de deneyip sonra tv'niz için tercih ettiğiniz bir cihaz bulabilirsiniz. https://github.com/LizardByte/Sunshine/releases/ buradan sunshine server'ı host pc'ye indirip kurmanız lazım. sonra her stream yaparken bu çalıştırılacak. geforce game experience'dan direkt yapmak da mümkün ama onda ayarlar yok aynı şekilde ve sadece nvidia kartları deniyor. kısacası bunu kurun ve kullanın. steam link de var ios ve android'de ama moonlight kalite, performans ve ince ayarları denediğim kadarıyla daha iyi, ayrıca steam link'in sıkıntı yaşadığı oyunlarda sorun yok mesela persona 5 royal winstore versiyonu gibi. client alete (box, stick, mini pc etc) moonlight'ı kuruyoruz, client aletinize göre seçin: https://github.com/moonlight-stream/moonlight-qt/releases https://play.google.com/store/apps/details?id=com.limelight&hl=en&gl=US https://apps.apple.com/us/app/moonlight-game-streaming/id1000551566 yalnız gamepad için usb çıkışı yoksa bluetooth ile client'a eşleşebilen bir client alet olması lazım, android tv'li seçenekler yapabiliyor okuduğum kadarıyla. xiaomi için ikinci nesil dualshock 4 controller'lar eşleştirilebiliyor denmiş, bende vardı ve eşleştirdim. aynı durumdaysanız controller versiyonunuzu şuradan kontrol edin: https://www.consoleskins.nl/en/manuals/what-generation-controller-do-i-have/playstation-4/ host'a ayrıca stick'e bağlı controller'ı tanıyabilmesi için vigembus driver kurmam gerekti. isim hakkı çakışması nedeniyle bu yıl içinde iptal edilmiş proje, güncelleme gelmeyecek ama şu anda win11'de çalışıyor: https://ds4-windows.com/download/vigembus-driver/ şimdi performans meselesine gelince, bu xiaomi stick'te ethernet yoktu. ethernet'li bulabildiğim aletler ya daha eski ya daha pahalı. wi-fi kaynağıyla arasında bir duvar olunca iyi performans alamadım. bitrate, latency filan hepsi kötüydü. ne var ki stick'in hemen 5-10cm dibinde powerline adapter üzerinden router'a direkt bağlı bir wifi mesh extender bulunduğundan ona bağladığım zaman bu sorun tamamen çözüldü. bu ne demek, ya ethernet ile direkt bağlayacaksınız client aleti ya da wi-fi kaynağının dibinde olması lazım. xiaomi tv stick 4k için çalıştığı söylenen şöyle bir ethernet adaptörü (şu an 300₺ civarı) buldum ama denemediğim için kefil olamıyorum: https://ugreen.com.tr/urun/ugreen-micro-usb-ethernet-donusturucu-adaptor/ nihayetinde aradığım yeni streaming çözümü bu oldu, keşke geçen yıl alsaymışım. yuzu'da fire emblem three houses ve persona 5 royal windows store versiyonunu denedim, gayet güzel sorunsuz oynanıyorlar. statlardan okuduğum kadarıyla 5-10ms encoding, 5ms network, 10ms de decoding delay var ama ben gecikme hissetmedim ama bunlar turn-based oyunlar tabii. competitive multiplayer olmadığı sürece kimsenin üzüleceğini sanmıyorum. iyi oyunlar. @Goeringilk önermişti geçen yıl, ona da teşekkür ederim. 2 3
MrLevie Mesaj tarihi: Ocak 10, 2025 Mesaj tarihi: Ocak 10, 2025 Buna biraz kafa patlatiyorum son zamanlarda. Bir iki ekleme yapayim @Sam izin verirse. Moonlighti headless calistiramiyorsunuz normalde, arka planda monitor istiyor. Bu arkadas bir virtual monitor driveri koyup sikintiyi cozmus. ikinci problem login konusu, windows login gerekiyor her actigimizda. Dolayisiyla otomatik login lazim. Bunun icin bir ton documentation var windowsun kendi sitesinde, fakat bazi kisiler icin bazi ayarlar aktif degil. Regedit uzerinden bazi cozumler var. Bazilari icin de direkman github. DYOR. 1
Sam Mesaj tarihi: Ocak 10, 2025 Konuyu açan Mesaj tarihi: Ocak 10, 2025 (düzenlendi) bende monitör kapanınca gpu çalıştırmayı durduruyor diye monitörün source'unu boş bir hdmi girişine değiştiriyorum, monitör sinyal gelmiyor diye uykuya giriyor ama gpu uyumuyor. tamamen monitörsüz çalıştırmak istediysen çözüm bulmuşsun iyi bari. herkes deneyimini, çözümünü, sorusunu paylaşsın tabii. hala kullanıyorum bu arada ben, yeni denemek isteyen varsa rehber hala geçerli bugün. ha bir güncelleme: gpu'yu yenileyince av1 encoding'e geçtim, xiaomi stick hardware av1 decoding yapabiliyor artık onu kullanıyorum. daha da iyi oldu. Ocak 10, 2025 Sam tarafından düzenlendi
µh Mesaj tarihi: Ocak 10, 2025 Mesaj tarihi: Ocak 10, 2025 Sam, 21.10.2023 18:16 tarihinde dedi ki: steam link de var ios ve android'de ama moonlight kalite, performans ve ince ayarları denediğim kadarıyla daha iyi, ayrıca steam link'in sıkıntı yaşadığı oyunlarda sorun yok mesela persona 5 royal winstore versiyonu gibi. client alete (box, stick, mini pc etc) moonlight'ı kuruyoruz, client aletinize göre seçin: Enteresan bir şekilde Pc'den steam deck'e aktarırken bile insanlar moonlight/sunshine kullanıyor steamlink yerine, pek çok kişi daha performanslı olduğunu söylüyor.
Sam Mesaj tarihi: Ocak 10, 2025 Konuyu açan Mesaj tarihi: Ocak 10, 2025 steamlink buna kadar iyiydi ama bunda çok daha fazla ayar var ve daha yüksek kaliteye ve performansa ulaşmak mümkün. steamlink’te sıkıntı yaşadığım durumlar oldu p5r winstore sürümünü oynamaya çalışınca mesela, moonlight/sunshine’da şimdiye kadar sorun çıkaran hiç örneğe rastlamadım. nvidia kendi kullanmayı bıraktıysa da kuvvetli bir altyapı yapılmış belli ki. cep telefonuna cp77 bile yansıttım steam’den sorunsuz. valve steamlink işini saldı biraz, mouse cursor kayboluyordu tizen tv app’de 3-4 yıl fixlemediler sonunda samsung kaldırana kadar. ikisini de kullanabilirsiniz ama ben steamlink’e dönmek için sebep göremedim.
deadwoll Mesaj tarihi: Ocak 10, 2025 Mesaj tarihi: Ocak 10, 2025 Ben Apple TV 4k aldim streaming icin gecen yaz. Steamlink bayagi sorunsuz calisiyor, ethernetten bagliyim Apple TV’ye ama PC wifi guclendiriciye ethernetten bagli oldugundan en mukemmel opsiyona sahip degilim. Buna ragmen baglanti kalitesi iyi ve genel olarak oyunlar sorunsuz calisiyor . Bir tek Rogue Trader’da sesle ilgili bir sorun oluyordu arada sanki ama tam hatirlayamiyorum. Onun haricinde Apple TV’de retroarch uzerinden 3ds oyunlarini oynamak istiyordum ama 3ds oynatamiyormus henuz retroarch apple tvlerde. O yuzden gittim steam’e retroarch kurdum, steamlink uzerinden tv’ye oyle yansitiyorum 3ds oyunlarini. Stream’le ugrasmak zorunda kalmasam iyiydi ama nasip kismet.
xaty Mesaj tarihi: Eylül 19, 2025 Mesaj tarihi: Eylül 19, 2025 µh, 10.01.2025 23:15 tarihinde dedi ki: insanlar moonlight/sunshine kullanıyor steamlink yerine Kesinlikle daha performanslı. TV'deki Android Box'a arka odadan oyun stream edip oynama işine bayadır kafayı takmıştım. Xbox Cloud bir nebze işimi görüyor ama AndroidBox bazen zayıf kalıyordu, Steam Link denedim ama çift controller beceremedim ayrıca kalite ve lag çok dert oldu. Moonshine deniyim dedim, süper oldu. Bir de Playnite kurdum (niye kullanmamışım bunca zamandır). Aslında hepsinin bir sürü ayarı, eklentisi, kurcalanacak şeyleri var ama fazla detaya girmeden hızlıca çalışır hale geldi. Gayet yüksek çözünürlük ve FPS ile TV'de oynayabiliyorum artık. Playnite ile emulatör oyunlarının da diğer oyunlarla yan yana durması, oynanabilir olması da ayrıca güzel oldu. 1
MrLevie Mesaj tarihi: Temmuz 2, 2026 Mesaj tarihi: Temmuz 2, 2026 Ben rahat durmadim Claude'a otomatik oyunlari scan edip direkt Moonlight'ta gosterecek hale getiren bir kod yazdirdim. Bilgisayardaki oyunlari tariyor, Moonlight ekraninda secebilmenizi sagliyor. SteamGridDB APIsi varsa eger oradan artwork de cekiyor. Isteyen calistirsin buyrun; Spoiler """ scan_games.py — Moonlight/Sunshine Game Scanner for Windows Scans Steam, Epic Games, GOG, and Xbox Game Pass installations, fetches box art from SteamGridDB, and writes sunshine apps.json. Usage: python scan_games.py # scan + fetch art + write config python scan_games.py --api-key YOUR_KEY # provide SteamGridDB key inline python scan_games.py --no-art # skip artwork fetching python scan_games.py --dry-run # preview JSON without writing python scan_games.py --merge # keep existing manual entries python scan_games.py --out PATH # custom output path python scan_games.py --sources steam epic # scan specific stores only python scan_games.py --help """ import argparse import json import os import shutil import sys import time import urllib.request import urllib.parse import urllib.error import winreg from dataclasses import dataclass, field from pathlib import Path from typing import Optional # ────────────────────────────────────────────────────────────── # Config # ────────────────────────────────────────────────────────────── # Where to store downloaded box art (next to this script) ART_DIR = Path(__file__).parent / "sunshine_art" # SteamGridDB API base SGDB_BASE = "https://www.steamgriddb.com/api/v2" # Preferred image dimensions for Sunshine (portrait box art, 2:3 ratio) # SteamGridDB grid dimensions we'll prefer: 600x900 or 460x690 PREFERRED_DIMENSIONS = ["600x900", "460x690", "342x482"] # Seconds to wait between SteamGridDB requests (respect rate limits) SGDB_RATE_DELAY = 0.25 # File where we persist the API key so you don't have to re-enter it KEY_FILE = Path(__file__).parent / ".sgdb_key" # ────────────────────────────────────────────────────────────── # Data model # ────────────────────────────────────────────────────────────── @dataclass class Game: name: str exe: str working_dir: str source: str launch_cmd: str = "" image: str = "" steam_app_id: str = "" # kept for direct Steam CDN fallback def to_sunshine_entry(self) -> dict: return { "name": self.name, "output": "", "cmd": self.launch_cmd if self.launch_cmd else self.exe, "working-dir": self.working_dir, "image-path": self.image, } # ────────────────────────────────────────────────────────────── # Helpers # ────────────────────────────────────────────────────────────── def reg_read(hive, path: str, key: str) -> Optional[str]: try: with winreg.OpenKey(hive, path) as k: value, _ = winreg.QueryValueEx(k, key) return str(value) except Exception: return None def reg_subkeys(hive, path: str) -> list[str]: try: with winreg.OpenKey(hive, path) as k: i, keys = 0, [] while True: try: keys.append(winreg.EnumKey(k, i)); i += 1 except OSError: break return keys except Exception: return [] def http_get_json(url: str, headers: dict) -> Optional[dict]: req = urllib.request.Request(url, headers=headers) try: with urllib.request.urlopen(req, timeout=10) as r: return json.loads(r.read().decode()) except Exception: return None def http_download(url: str, dest: Path) -> bool: try: req = urllib.request.Request(url, headers={"User-Agent": "sunshine-game-scanner/1.0"}) with urllib.request.urlopen(req, timeout=15) as r: dest.write_bytes(r.read()) return True except Exception: return False def vdf_parse(text: str) -> dict: def tokenize(s): tokens, i = [], 0 while i < len(s): c = s[i] if c in " \t\r\n": i += 1 elif c == '"': j = i + 1 while j < len(s) and not (s[j] == '"' and s[j-1] != '\\'): j += 1 tokens.append(s[i+1:j]); i = j + 1 elif c in '{}': tokens.append(c); i += 1 elif c == '/': end = s.find('\n', i); i = end if end != -1 else len(s) else: j = i while j < len(s) and s[j] not in ' \t\r\n{}': j += 1 tokens.append(s[i:j]); i = j return tokens tokens = tokenize(text) pos = [0] def parse_block(): obj = {} while pos[0] < len(tokens): tok = tokens[pos[0]] if tok == '}': pos[0] += 1; return obj key = tok; pos[0] += 1 if pos[0] >= len(tokens): break nxt = tokens[pos[0]] if nxt == '{': pos[0] += 1; obj[key] = parse_block() else: obj[key] = nxt; pos[0] += 1 return obj if tokens and tokens[0] == '{': pos[0] += 1; return parse_block() result = {} while pos[0] < len(tokens): key = tokens[pos[0]]; pos[0] += 1 if pos[0] < len(tokens): nxt = tokens[pos[0]] if nxt == '{': pos[0] += 1; result[key] = parse_block() else: result[key] = nxt; pos[0] += 1 return result def safe_filename(name: str) -> str: """Turn a game name into a safe filename.""" return "".join(c if c.isalnum() or c in " -_()" else "_" for c in name).strip() # ────────────────────────────────────────────────────────────── # SteamGridDB artwork fetcher # ────────────────────────────────────────────────────────────── class SteamGridDB: def __init__(self, api_key: str): self.headers = { "Authorization": f"Bearer {api_key}", "User-Agent": "sunshine-game-scanner/1.0", } self._last_request = 0.0 def _throttle(self): elapsed = time.monotonic() - self._last_request if elapsed < SGDB_RATE_DELAY: time.sleep(SGDB_RATE_DELAY - elapsed) self._last_request = time.monotonic() def search_game(self, name: str) -> Optional[int]: """Return the first SGDB game ID matching the name.""" self._throttle() url = f"{SGDB_BASE}/search/autocomplete/{urllib.parse.quote(name)}" data = http_get_json(url, self.headers) if data and data.get("success") and data.get("data"): return data["data"][0]["id"] return None def get_game_by_steam_id(self, steam_id: str) -> Optional[int]: """Look up SGDB game ID directly via Steam app ID (faster, more accurate).""" self._throttle() url = f"{SGDB_BASE}/games/steam/{steam_id}" data = http_get_json(url, self.headers) if data and data.get("success") and data.get("data"): return data["data"]["id"] return None def get_grid_image_url(self, game_id: int) -> Optional[str]: """ Fetch portrait grid images for a game. Prefers 600x900 or 460x690, falls back to any available. Skips animated (WebP) images — Sunshine doesn't render them. """ self._throttle() url = f"{SGDB_BASE}/grids/game/{game_id}?dimensions={','.join(PREFERRED_DIMENSIONS)}&mime=png,jpeg" data = http_get_json(url, self.headers) if not data or not data.get("success") or not data.get("data"): # Retry without dimension filter self._throttle() url = f"{SGDB_BASE}/grids/game/{game_id}?mime=png,jpeg" data = http_get_json(url, self.headers) if data and data.get("success") and data.get("data"): images = data["data"] # Prefer non-animated, highest score first (API returns sorted by score) for img in images: if img.get("mime") in ("image/png", "image/jpeg"): return img.get("url") return None def fetch_art_for_game(self, game: Game, art_dir: Path) -> Optional[Path]: """ Resolve and download box art for a game. Returns local path if successful, None otherwise. """ fname = safe_filename(game.name) # Check if already downloaded for ext in ("png", "jpg"): existing = art_dir / f"{fname}.{ext}" if existing.exists(): return existing # Resolve SGDB game ID sgdb_id = None if game.steam_app_id: sgdb_id = self.get_game_by_steam_id(game.steam_app_id) if not sgdb_id: sgdb_id = self.search_game(game.name) if not sgdb_id: return None image_url = self.get_grid_image_url(sgdb_id) if not image_url: return None ext = "png" if "png" in image_url.lower() else "jpg" dest = art_dir / f"{fname}.{ext}" if http_download(image_url, dest): return dest return None def try_steam_cdn_fallback(game: Game, art_dir: Path) -> Optional[Path]: """ For Steam games: try the Steam CDN directly for the library header image (460x215) — not ideal portrait art, but better than nothing and requires no API key. """ if not game.steam_app_id: return None fname = safe_filename(game.name) dest = art_dir / f"{fname}_header.jpg" if dest.exists(): return dest url = f"https://cdn.cloudflare.steamstatic.com/steam/apps/{game.steam_app_id}/library_600x900_2x.jpg" if http_download(url, dest): return dest # Fallback to standard header url2 = f"https://cdn.cloudflare.steamstatic.com/steam/apps/{game.steam_app_id}/header.jpg" if http_download(url2, dest): return dest return None # ────────────────────────────────────────────────────────────── # Steam scanner # ────────────────────────────────────────────────────────────── STEAM_REG_PATHS = [ (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Valve\Steam"), (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Valve\Steam"), (winreg.HKEY_CURRENT_USER, r"SOFTWARE\Valve\Steam"), ] def find_steam_root() -> Optional[Path]: for hive, path in STEAM_REG_PATHS: val = reg_read(hive, path, "InstallPath") if val and Path(val).exists(): return Path(val) for candidate in [ Path(os.environ.get("ProgramFiles(x86)", "C:/Program Files (x86)")) / "Steam", Path(os.environ.get("ProgramFiles", "C:/Program Files")) / "Steam", ]: if candidate.exists(): return candidate return None def get_steam_libraries(steam_root: Path) -> list[Path]: libs = [steam_root / "steamapps"] for vdf_path in [ steam_root / "steamapps" / "libraryfolders.vdf", steam_root / "config" / "libraryfolders.vdf", ]: if vdf_path.exists(): try: data = vdf_parse(vdf_path.read_text(encoding="utf-8", errors="replace")) folders = data.get("libraryfolders", data.get("LibraryFolders", data)) for k, v in folders.items(): if isinstance(v, dict): p = v.get("path") if p: c = Path(p) / "steamapps" if c.exists(): libs.append(c) elif isinstance(v, str) and k.isdigit(): c = Path(v) / "steamapps" if c.exists(): libs.append(c) except Exception: pass break return list(dict.fromkeys(libs)) def scan_steam(steam_root: Path) -> list[Game]: games = [] for lib in get_steam_libraries(steam_root): for acf in lib.glob("appmanifest_*.acf"): try: data = vdf_parse(acf.read_text(encoding="utf-8", errors="replace")) state = data.get("AppState", data) app_id = state.get("appid", "") name = state.get("name", "") install_dir = state.get("installdir", "") if not name or not app_id: continue game_dir = lib / "common" / install_dir launch_cmd = f"steam://rungameid/{app_id}" exe_path = "" if game_dir.exists(): exes = [e for e in game_dir.rglob("*.exe") if not any(s in e.stem.lower() for s in {"uninstall","setup","redist","vc_redist","dxsetup", "crashreport","crashhandler","launch","launcher", "helper","updater","unins000","uninst","easyanticheat"})] root_exes = [e for e in exes if e.parent == game_dir] exe_path = str((root_exes or exes)[0]) if (root_exes or exes) else "" games.append(Game( name=name, exe=exe_path or launch_cmd, working_dir=str(game_dir) if game_dir.exists() else "", source="steam", launch_cmd=launch_cmd, steam_app_id=app_id, )) except Exception: continue return games # ────────────────────────────────────────────────────────────── # Epic Games scanner # ────────────────────────────────────────────────────────────── EPIC_MANIFEST_DIRS = [ Path(os.environ.get("ProgramData", "C:/ProgramData")) / "Epic/EpicGamesLauncher/Data/Manifests", ] def scan_epic() -> list[Game]: games = [] for manifest_dir in EPIC_MANIFEST_DIRS: if not manifest_dir.exists(): continue for item_path in manifest_dir.glob("*.item"): try: data = json.loads(item_path.read_text(encoding="utf-8", errors="replace")) name = data.get("DisplayName", "") install_location = data.get("InstallLocation", "") launch_exe = data.get("LaunchExecutable", "") catalog_id = data.get("CatalogItemId", "") namespace = data.get("CatalogNamespace", "") app_name = data.get("AppName", "") if not name or not install_location: continue install_dir = Path(install_location) exe_full = install_dir / launch_exe if launch_exe else None launch_cmd = ( f"com.epicgames.launcher://apps/{namespace}%3A{catalog_id}%3A{app_name}?action=launch&silent=true" if namespace and catalog_id and app_name else "" ) games.append(Game( name=name, exe=str(exe_full) if exe_full and exe_full.exists() else (launch_cmd or install_location), working_dir=str(install_dir) if install_dir.exists() else "", source="epic", launch_cmd=launch_cmd, )) except Exception: continue return games # ────────────────────────────────────────────────────────────── # GOG scanner # ────────────────────────────────────────────────────────────── GOG_REG_PATHS = [ (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\GOG.com\Games"), (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\GOG.com\Games"), ] def scan_gog() -> list[Game]: games = [] for hive, base_path in GOG_REG_PATHS: for gog_id in reg_subkeys(hive, base_path): path = f"{base_path}\\{gog_id}" name = reg_read(hive, path, "GAMENAME") or reg_read(hive, path, "gameName") exe = reg_read(hive, path, "EXE") or reg_read(hive, path, "exe") workdir = reg_read(hive, path, "WORKINGDIR") or reg_read(hive, path, "workingDir") if not name or not exe: continue games.append(Game( name=name, exe=exe, working_dir=workdir or str(Path(exe).parent), source="gog", launch_cmd=f"goggalaxy://openGame/{gog_id}", )) return games # ────────────────────────────────────────────────────────────── # Xbox / Microsoft Store scanner # ────────────────────────────────────────────────────────────── XBOX_PACKAGE_REG = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" XBOX_APPS_DIR = Path("C:/XboxGames") def scan_xbox() -> list[Game]: games = [] for hive in (winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER): for subkey in reg_subkeys(hive, XBOX_PACKAGE_REG): path = f"{XBOX_PACKAGE_REG}\\{subkey}" publisher = reg_read(hive, path, "Publisher") or "" if "Xbox" not in publisher and "Microsoft" not in publisher: continue name = reg_read(hive, path, "DisplayName") or "" install_loc = reg_read(hive, path, "InstallLocation") or "" if not name or not install_loc: continue if name.startswith(("Microsoft Visual", "Microsoft .NET", "Microsoft Edge")): continue install_dir = Path(install_loc) exe_path = "" if install_dir.exists(): exes = sorted( [e for e in install_dir.rglob("*.exe") if not any(s in e.stem.lower() for s in {"setup","install","uninstall","helper","crash","launcher", "redist","vc_redist","update"})], key=lambda p: len(p.parts) ) if exes: exe_path = str(exes[0]) games.append(Game( name=name, exe=exe_path or install_loc, working_dir=str(install_dir) if install_dir.exists() else "", source="xbox", )) if XBOX_APPS_DIR.exists(): seen = {g.name.lower() for g in games} for game_folder in XBOX_APPS_DIR.iterdir(): if not game_folder.is_dir() or game_folder.name.lower() in seen: continue exes = sorted( [e for e in game_folder.rglob("*.exe") if not any(s in e.stem.lower() for s in {"setup","install","uninstall","crash","helper"})], key=lambda p: len(p.parts) ) if not exes: continue games.append(Game( name=game_folder.name, exe=str(exes[0]), working_dir=str(exes[0].parent), source="xbox", )) return games # ────────────────────────────────────────────────────────────── # Artwork pipeline # ────────────────────────────────────────────────────────────── def load_api_key(cli_key: Optional[str]) -> Optional[str]: """Return API key from: CLI arg → saved key file → interactive prompt.""" if cli_key: return cli_key.strip() if KEY_FILE.exists(): key = KEY_FILE.read_text(encoding="utf-8").strip() if key: return key # Interactive prompt print("\n[?] SteamGridDB API key not found.") print(" Get a free key at: https://www.steamgriddb.com/api/v2") print(" (Log in → click your avatar → API Key)") print(" Leave blank to skip artwork and use --no-art next time.\n") key = input(" Paste your API key here: ").strip() if key: KEY_FILE.write_text(key, encoding="utf-8") print(f" [✓] Key saved to {KEY_FILE} — won't ask again.") return key or None def fetch_artwork(games: list[Game], api_key: str) -> None: """ Download box art for all games in-place (sets game.image). Uses SteamGridDB with a Steam CDN fallback for Steam titles. """ ART_DIR.mkdir(parents=True, exist_ok=True) sgdb = SteamGridDB(api_key) total = len(games) ok = skipped = failed = 0 print(f"\n[*] Fetching artwork for {total} game(s)...") print(f" Art cache: {ART_DIR}\n") for i, game in enumerate(games, 1): prefix = f" [{i:>{len(str(total))}}/{total}]" fname = safe_filename(game.name) # Already have art (from a previous run) for ext in ("png", "jpg"): cached = ART_DIR / f"{fname}.{ext}" if cached.exists(): game.image = str(cached) skipped += 1 print(f"{prefix} (cached) {game.name}") break if game.image: continue # Try SteamGridDB art_path = sgdb.fetch_art_for_game(game, ART_DIR) if art_path: game.image = str(art_path) ok += 1 print(f"{prefix} [SGDB ✓] {game.name}") continue # Steam CDN fallback (no API key needed) if game.steam_app_id: art_path = try_steam_cdn_fallback(game, ART_DIR) if art_path: game.image = str(art_path) ok += 1 print(f"{prefix} [CDN ✓] {game.name}") continue failed += 1 print(f"{prefix} [miss ] {game.name}") print(f"\n Done — {ok} downloaded, {skipped} cached, {failed} not found.") # ────────────────────────────────────────────────────────────── # Sunshine config # ────────────────────────────────────────────────────────────── def find_existing_sunshine_config() -> Optional[Path]: candidates = [ Path(os.environ.get("ProgramFiles", "C:/Program Files")) / "Sunshine/config/apps.json", Path(os.environ.get("APPDATA", "")) / "Sunshine/config/apps.json", Path(os.environ.get("LOCALAPPDATA", "")) / "Sunshine/config/apps.json", Path("C:/Program Files/Sunshine/config/apps.json"), ] for c in candidates: if c.exists(): return c return None def build_sunshine_config(games: list[Game]) -> dict: apps, seen = [], set() for g in sorted(games, key=lambda x: x.name.lower()): key = g.name.lower().strip() if key in seen: continue seen.add(key) apps.append(g.to_sunshine_entry()) return {"apps": apps, "env": {"PATH": "$(PATH):$(HOME)/.local/bin"}} # ────────────────────────────────────────────────────────────── # Main # ────────────────────────────────────────────────────────────── def main(): parser = argparse.ArgumentParser( description="Scan installed games and generate a Sunshine apps.json", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__, ) parser.add_argument("--api-key", metavar="KEY", help="SteamGridDB API key (saved for future runs)") parser.add_argument("--no-art", action="store_true", help="Skip artwork fetching entirely") parser.add_argument("--dry-run", action="store_true", help="Print the JSON without writing to disk") parser.add_argument("--out", metavar="PATH", help="Output path (default: sunshine_apps.json next to this script)") parser.add_argument("--merge", action="store_true", help="Merge with existing Sunshine apps.json (keeps manual entries)") parser.add_argument("--sources", metavar="SOURCE", nargs="+", choices=["steam", "epic", "gog", "xbox"], default=["steam", "epic", "gog", "xbox"], help="Which stores to scan (default: all)") args = parser.parse_args() games: list[Game] = [] if "steam" in args.sources: print("[*] Scanning Steam...") steam_root = find_steam_root() if steam_root: found = scan_steam(steam_root) print(f" Found {len(found)} Steam game(s) [root: {steam_root}]") games.extend(found) else: print(" Steam not found — skipping.") if "epic" in args.sources: print("[*] Scanning Epic Games...") found = scan_epic() print(f" Found {len(found)} Epic game(s)") games.extend(found) if "gog" in args.sources: print("[*] Scanning GOG...") found = scan_gog() print(f" Found {len(found)} GOG game(s)") games.extend(found) if "xbox" in args.sources: print("[*] Scanning Xbox / Microsoft Store...") found = scan_xbox() print(f" Found {len(found)} Xbox/MS game(s)") games.extend(found) print(f"\n[+] Total: {len(games)} game(s) found") if not games: print("[!] No games found. Check that your launchers are installed.") sys.exit(0) # ── Artwork ─────────────────────────────────────────────── if not args.no_art: api_key = load_api_key(args.api_key) if api_key: fetch_artwork(games, api_key) else: print("\n[!] No API key — skipping artwork. Use --no-art to suppress this message.") else: print("[*] Artwork skipped (--no-art).") # ── Build config ────────────────────────────────────────── config = build_sunshine_config(games) if args.merge: existing_path = find_existing_sunshine_config() if existing_path: try: existing = json.loads(existing_path.read_text(encoding="utf-8")) manual_entries = [a for a in existing.get("apps", []) if a["name"].lower() not in {g.name.lower() for g in games}] config["apps"] = manual_entries + config["apps"] print(f"\n[+] Merged {len(manual_entries)} existing manual entry/entries.") except Exception as e: print(f"\n[!] Could not read existing config: {e}") json_out = json.dumps(config, indent=2, ensure_ascii=False) if args.dry_run: print(json_out) print("\n[dry-run] Nothing written.") return # ── Write output ────────────────────────────────────────── out_path = Path(args.out) if args.out else Path(__file__).parent / "sunshine_apps.json" out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(json_out, encoding="utf-8") print(f"\n[✓] Written to: {out_path}") print(f" {len(config['apps'])} app entries total.") # ── Auto-copy to Sunshine ───────────────────────────────── sunshine_path = find_existing_sunshine_config() if sunshine_path: backup_path = sunshine_path.with_suffix(".json.bak") try: if sunshine_path.exists(): shutil.copy2(sunshine_path, backup_path) print(f"[+] Backed up existing config → {backup_path}") except Exception as e: print(f"[!] Could not create backup: {e}") try: sunshine_path.write_text(json_out, encoding="utf-8") print(f"[✓] Copied to Sunshine config: {sunshine_path}") print("\n[!] Restart Sunshine for the new app list to take effect.") print(" Sunshine web UI: https://localhost:47990") except PermissionError: print(f"\n[!] Permission denied writing to {sunshine_path}") print(" Re-run this script as Administrator.") except Exception as e: print(f"\n[!] Could not copy to Sunshine config: {e}") else: print("\n[!] Sunshine config directory not found — could not auto-copy.") print(f" Manually place {out_path} at:") print(" C:\\Program Files\\Sunshine\\config\\apps.json") print(" Then restart Sunshine.") if __name__ == "__main__": main()
Goering Mesaj tarihi: Temmuz 3, 2026 Mesaj tarihi: Temmuz 3, 2026 (düzenlendi) apollo fork'una bakmanızı tavsiye ediyorum https://github.com/ClassicOldSong/Apollo Temmuz 3, 2026 Goering tarafından düzenlendi
Goering Mesaj tarihi: Temmuz 3, 2026 Mesaj tarihi: Temmuz 3, 2026 (düzenlendi) MrLevie, 7/3/2026 12:36 AM tarihinde dedi ki: Ben rahat durmadim Claude'a otomatik oyunlari scan edip direkt Moonlight'ta gosterecek hale getiren bir kod yazdirdim. Bilgisayardaki oyunlari tariyor, Moonlight ekraninda secebilmenizi sagliyor. SteamGridDB APIsi varsa eger oradan artwork de cekiyor. Isteyen calistirsin buyrun; Gizli içeriği göster """ scan_games.py — Moonlight/Sunshine Game Scanner for Windows Scans Steam, Epic Games, GOG, and Xbox Game Pass installations, fetches box art from SteamGridDB, and writes sunshine apps.json. Usage: python scan_games.py # scan + fetch art + write config python scan_games.py --api-key YOUR_KEY # provide SteamGridDB key inline python scan_games.py --no-art # skip artwork fetching python scan_games.py --dry-run # preview JSON without writing python scan_games.py --merge # keep existing manual entries python scan_games.py --out PATH # custom output path python scan_games.py --sources steam epic # scan specific stores only python scan_games.py --help """ import argparse import json import os import shutil import sys import time import urllib.request import urllib.parse import urllib.error import winreg from dataclasses import dataclass, field from pathlib import Path from typing import Optional # ────────────────────────────────────────────────────────────── # Config # ────────────────────────────────────────────────────────────── # Where to store downloaded box art (next to this script) ART_DIR = Path(__file__).parent / "sunshine_art" # SteamGridDB API base SGDB_BASE = "https://www.steamgriddb.com/api/v2" # Preferred image dimensions for Sunshine (portrait box art, 2:3 ratio) # SteamGridDB grid dimensions we'll prefer: 600x900 or 460x690 PREFERRED_DIMENSIONS = ["600x900", "460x690", "342x482"] # Seconds to wait between SteamGridDB requests (respect rate limits) SGDB_RATE_DELAY = 0.25 # File where we persist the API key so you don't have to re-enter it KEY_FILE = Path(__file__).parent / ".sgdb_key" # ────────────────────────────────────────────────────────────── # Data model # ────────────────────────────────────────────────────────────── @dataclass class Game: name: str exe: str working_dir: str source: str launch_cmd: str = "" image: str = "" steam_app_id: str = "" # kept for direct Steam CDN fallback def to_sunshine_entry(self) -> dict: return { "name": self.name, "output": "", "cmd": self.launch_cmd if self.launch_cmd else self.exe, "working-dir": self.working_dir, "image-path": self.image, } # ────────────────────────────────────────────────────────────── # Helpers # ────────────────────────────────────────────────────────────── def reg_read(hive, path: str, key: str) -> Optional[str]: try: with winreg.OpenKey(hive, path) as k: value, _ = winreg.QueryValueEx(k, key) return str(value) except Exception: return None def reg_subkeys(hive, path: str) -> list[str]: try: with winreg.OpenKey(hive, path) as k: i, keys = 0, [] while True: try: keys.append(winreg.EnumKey(k, i)); i += 1 except OSError: break return keys except Exception: return [] def http_get_json(url: str, headers: dict) -> Optional[dict]: req = urllib.request.Request(url, headers=headers) try: with urllib.request.urlopen(req, timeout=10) as r: return json.loads(r.read().decode()) except Exception: return None def http_download(url: str, dest: Path) -> bool: try: req = urllib.request.Request(url, headers={"User-Agent": "sunshine-game-scanner/1.0"}) with urllib.request.urlopen(req, timeout=15) as r: dest.write_bytes(r.read()) return True except Exception: return False def vdf_parse(text: str) -> dict: def tokenize(s): tokens, i = [], 0 while i < len(s): c = s[i] if c in " \t\r\n": i += 1 elif c == '"': j = i + 1 while j < len(s) and not (s[j] == '"' and s[j-1] != '\\'): j += 1 tokens.append(s[i+1:j]); i = j + 1 elif c in '{}': tokens.append(c); i += 1 elif c == '/': end = s.find('\n', i); i = end if end != -1 else len(s) else: j = i while j < len(s) and s[j] not in ' \t\r\n{}': j += 1 tokens.append(s[i:j]); i = j return tokens tokens = tokenize(text) pos = [0] def parse_block(): obj = {} while pos[0] < len(tokens): tok = tokens[pos[0]] if tok == '}': pos[0] += 1; return obj key = tok; pos[0] += 1 if pos[0] >= len(tokens): break nxt = tokens[pos[0]] if nxt == '{': pos[0] += 1; obj[key] = parse_block() else: obj[key] = nxt; pos[0] += 1 return obj if tokens and tokens[0] == '{': pos[0] += 1; return parse_block() result = {} while pos[0] < len(tokens): key = tokens[pos[0]]; pos[0] += 1 if pos[0] < len(tokens): nxt = tokens[pos[0]] if nxt == '{': pos[0] += 1; result[key] = parse_block() else: result[key] = nxt; pos[0] += 1 return result def safe_filename(name: str) -> str: """Turn a game name into a safe filename.""" return "".join(c if c.isalnum() or c in " -_()" else "_" for c in name).strip() # ────────────────────────────────────────────────────────────── # SteamGridDB artwork fetcher # ────────────────────────────────────────────────────────────── class SteamGridDB: def __init__(self, api_key: str): self.headers = { "Authorization": f"Bearer {api_key}", "User-Agent": "sunshine-game-scanner/1.0", } self._last_request = 0.0 def _throttle(self): elapsed = time.monotonic() - self._last_request if elapsed < SGDB_RATE_DELAY: time.sleep(SGDB_RATE_DELAY - elapsed) self._last_request = time.monotonic() def search_game(self, name: str) -> Optional[int]: """Return the first SGDB game ID matching the name.""" self._throttle() url = f"{SGDB_BASE}/search/autocomplete/{urllib.parse.quote(name)}" data = http_get_json(url, self.headers) if data and data.get("success") and data.get("data"): return data["data"][0]["id"] return None def get_game_by_steam_id(self, steam_id: str) -> Optional[int]: """Look up SGDB game ID directly via Steam app ID (faster, more accurate).""" self._throttle() url = f"{SGDB_BASE}/games/steam/{steam_id}" data = http_get_json(url, self.headers) if data and data.get("success") and data.get("data"): return data["data"]["id"] return None def get_grid_image_url(self, game_id: int) -> Optional[str]: """ Fetch portrait grid images for a game. Prefers 600x900 or 460x690, falls back to any available. Skips animated (WebP) images — Sunshine doesn't render them. """ self._throttle() url = f"{SGDB_BASE}/grids/game/{game_id}?dimensions={','.join(PREFERRED_DIMENSIONS)}&mime=png,jpeg" data = http_get_json(url, self.headers) if not data or not data.get("success") or not data.get("data"): # Retry without dimension filter self._throttle() url = f"{SGDB_BASE}/grids/game/{game_id}?mime=png,jpeg" data = http_get_json(url, self.headers) if data and data.get("success") and data.get("data"): images = data["data"] # Prefer non-animated, highest score first (API returns sorted by score) for img in images: if img.get("mime") in ("image/png", "image/jpeg"): return img.get("url") return None def fetch_art_for_game(self, game: Game, art_dir: Path) -> Optional[Path]: """ Resolve and download box art for a game. Returns local path if successful, None otherwise. """ fname = safe_filename(game.name) # Check if already downloaded for ext in ("png", "jpg"): existing = art_dir / f"{fname}.{ext}" if existing.exists(): return existing # Resolve SGDB game ID sgdb_id = None if game.steam_app_id: sgdb_id = self.get_game_by_steam_id(game.steam_app_id) if not sgdb_id: sgdb_id = self.search_game(game.name) if not sgdb_id: return None image_url = self.get_grid_image_url(sgdb_id) if not image_url: return None ext = "png" if "png" in image_url.lower() else "jpg" dest = art_dir / f"{fname}.{ext}" if http_download(image_url, dest): return dest return None def try_steam_cdn_fallback(game: Game, art_dir: Path) -> Optional[Path]: """ For Steam games: try the Steam CDN directly for the library header image (460x215) — not ideal portrait art, but better than nothing and requires no API key. """ if not game.steam_app_id: return None fname = safe_filename(game.name) dest = art_dir / f"{fname}_header.jpg" if dest.exists(): return dest url = f"https://cdn.cloudflare.steamstatic.com/steam/apps/{game.steam_app_id}/library_600x900_2x.jpg" if http_download(url, dest): return dest # Fallback to standard header url2 = f"https://cdn.cloudflare.steamstatic.com/steam/apps/{game.steam_app_id}/header.jpg" if http_download(url2, dest): return dest return None # ────────────────────────────────────────────────────────────── # Steam scanner # ────────────────────────────────────────────────────────────── STEAM_REG_PATHS = [ (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Valve\Steam"), (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Valve\Steam"), (winreg.HKEY_CURRENT_USER, r"SOFTWARE\Valve\Steam"), ] def find_steam_root() -> Optional[Path]: for hive, path in STEAM_REG_PATHS: val = reg_read(hive, path, "InstallPath") if val and Path(val).exists(): return Path(val) for candidate in [ Path(os.environ.get("ProgramFiles(x86)", "C:/Program Files (x86)")) / "Steam", Path(os.environ.get("ProgramFiles", "C:/Program Files")) / "Steam", ]: if candidate.exists(): return candidate return None def get_steam_libraries(steam_root: Path) -> list[Path]: libs = [steam_root / "steamapps"] for vdf_path in [ steam_root / "steamapps" / "libraryfolders.vdf", steam_root / "config" / "libraryfolders.vdf", ]: if vdf_path.exists(): try: data = vdf_parse(vdf_path.read_text(encoding="utf-8", errors="replace")) folders = data.get("libraryfolders", data.get("LibraryFolders", data)) for k, v in folders.items(): if isinstance(v, dict): p = v.get("path") if p: c = Path(p) / "steamapps" if c.exists(): libs.append(c) elif isinstance(v, str) and k.isdigit(): c = Path(v) / "steamapps" if c.exists(): libs.append(c) except Exception: pass break return list(dict.fromkeys(libs)) def scan_steam(steam_root: Path) -> list[Game]: games = [] for lib in get_steam_libraries(steam_root): for acf in lib.glob("appmanifest_*.acf"): try: data = vdf_parse(acf.read_text(encoding="utf-8", errors="replace")) state = data.get("AppState", data) app_id = state.get("appid", "") name = state.get("name", "") install_dir = state.get("installdir", "") if not name or not app_id: continue game_dir = lib / "common" / install_dir launch_cmd = f"steam://rungameid/{app_id}" exe_path = "" if game_dir.exists(): exes = [e for e in game_dir.rglob("*.exe") if not any(s in e.stem.lower() for s in {"uninstall","setup","redist","vc_redist","dxsetup", "crashreport","crashhandler","launch","launcher", "helper","updater","unins000","uninst","easyanticheat"})] root_exes = [e for e in exes if e.parent == game_dir] exe_path = str((root_exes or exes)[0]) if (root_exes or exes) else "" games.append(Game( name=name, exe=exe_path or launch_cmd, working_dir=str(game_dir) if game_dir.exists() else "", source="steam", launch_cmd=launch_cmd, steam_app_id=app_id, )) except Exception: continue return games # ────────────────────────────────────────────────────────────── # Epic Games scanner # ────────────────────────────────────────────────────────────── EPIC_MANIFEST_DIRS = [ Path(os.environ.get("ProgramData", "C:/ProgramData")) / "Epic/EpicGamesLauncher/Data/Manifests", ] def scan_epic() -> list[Game]: games = [] for manifest_dir in EPIC_MANIFEST_DIRS: if not manifest_dir.exists(): continue for item_path in manifest_dir.glob("*.item"): try: data = json.loads(item_path.read_text(encoding="utf-8", errors="replace")) name = data.get("DisplayName", "") install_location = data.get("InstallLocation", "") launch_exe = data.get("LaunchExecutable", "") catalog_id = data.get("CatalogItemId", "") namespace = data.get("CatalogNamespace", "") app_name = data.get("AppName", "") if not name or not install_location: continue install_dir = Path(install_location) exe_full = install_dir / launch_exe if launch_exe else None launch_cmd = ( f"com.epicgames.launcher://apps/{namespace}%3A{catalog_id}%3A{app_name}?action=launch&silent=true" if namespace and catalog_id and app_name else "" ) games.append(Game( name=name, exe=str(exe_full) if exe_full and exe_full.exists() else (launch_cmd or install_location), working_dir=str(install_dir) if install_dir.exists() else "", source="epic", launch_cmd=launch_cmd, )) except Exception: continue return games # ────────────────────────────────────────────────────────────── # GOG scanner # ────────────────────────────────────────────────────────────── GOG_REG_PATHS = [ (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\GOG.com\Games"), (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\GOG.com\Games"), ] def scan_gog() -> list[Game]: games = [] for hive, base_path in GOG_REG_PATHS: for gog_id in reg_subkeys(hive, base_path): path = f"{base_path}\\{gog_id}" name = reg_read(hive, path, "GAMENAME") or reg_read(hive, path, "gameName") exe = reg_read(hive, path, "EXE") or reg_read(hive, path, "exe") workdir = reg_read(hive, path, "WORKINGDIR") or reg_read(hive, path, "workingDir") if not name or not exe: continue games.append(Game( name=name, exe=exe, working_dir=workdir or str(Path(exe).parent), source="gog", launch_cmd=f"goggalaxy://openGame/{gog_id}", )) return games # ────────────────────────────────────────────────────────────── # Xbox / Microsoft Store scanner # ────────────────────────────────────────────────────────────── XBOX_PACKAGE_REG = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" XBOX_APPS_DIR = Path("C:/XboxGames") def scan_xbox() -> list[Game]: games = [] for hive in (winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER): for subkey in reg_subkeys(hive, XBOX_PACKAGE_REG): path = f"{XBOX_PACKAGE_REG}\\{subkey}" publisher = reg_read(hive, path, "Publisher") or "" if "Xbox" not in publisher and "Microsoft" not in publisher: continue name = reg_read(hive, path, "DisplayName") or "" install_loc = reg_read(hive, path, "InstallLocation") or "" if not name or not install_loc: continue if name.startswith(("Microsoft Visual", "Microsoft .NET", "Microsoft Edge")): continue install_dir = Path(install_loc) exe_path = "" if install_dir.exists(): exes = sorted( [e for e in install_dir.rglob("*.exe") if not any(s in e.stem.lower() for s in {"setup","install","uninstall","helper","crash","launcher", "redist","vc_redist","update"})], key=lambda p: len(p.parts) ) if exes: exe_path = str(exes[0]) games.append(Game( name=name, exe=exe_path or install_loc, working_dir=str(install_dir) if install_dir.exists() else "", source="xbox", )) if XBOX_APPS_DIR.exists(): seen = {g.name.lower() for g in games} for game_folder in XBOX_APPS_DIR.iterdir(): if not game_folder.is_dir() or game_folder.name.lower() in seen: continue exes = sorted( [e for e in game_folder.rglob("*.exe") if not any(s in e.stem.lower() for s in {"setup","install","uninstall","crash","helper"})], key=lambda p: len(p.parts) ) if not exes: continue games.append(Game( name=game_folder.name, exe=str(exes[0]), working_dir=str(exes[0].parent), source="xbox", )) return games # ────────────────────────────────────────────────────────────── # Artwork pipeline # ────────────────────────────────────────────────────────────── def load_api_key(cli_key: Optional[str]) -> Optional[str]: """Return API key from: CLI arg → saved key file → interactive prompt.""" if cli_key: return cli_key.strip() if KEY_FILE.exists(): key = KEY_FILE.read_text(encoding="utf-8").strip() if key: return key # Interactive prompt print("\n[?] SteamGridDB API key not found.") print(" Get a free key at: https://www.steamgriddb.com/api/v2") print(" (Log in → click your avatar → API Key)") print(" Leave blank to skip artwork and use --no-art next time.\n") key = input(" Paste your API key here: ").strip() if key: KEY_FILE.write_text(key, encoding="utf-8") print(f" [✓] Key saved to {KEY_FILE} — won't ask again.") return key or None def fetch_artwork(games: list[Game], api_key: str) -> None: """ Download box art for all games in-place (sets game.image). Uses SteamGridDB with a Steam CDN fallback for Steam titles. """ ART_DIR.mkdir(parents=True, exist_ok=True) sgdb = SteamGridDB(api_key) total = len(games) ok = skipped = failed = 0 print(f"\n[*] Fetching artwork for {total} game(s)...") print(f" Art cache: {ART_DIR}\n") for i, game in enumerate(games, 1): prefix = f" [{i:>{len(str(total))}}/{total}]" fname = safe_filename(game.name) # Already have art (from a previous run) for ext in ("png", "jpg"): cached = ART_DIR / f"{fname}.{ext}" if cached.exists(): game.image = str(cached) skipped += 1 print(f"{prefix} (cached) {game.name}") break if game.image: continue # Try SteamGridDB art_path = sgdb.fetch_art_for_game(game, ART_DIR) if art_path: game.image = str(art_path) ok += 1 print(f"{prefix} [SGDB ✓] {game.name}") continue # Steam CDN fallback (no API key needed) if game.steam_app_id: art_path = try_steam_cdn_fallback(game, ART_DIR) if art_path: game.image = str(art_path) ok += 1 print(f"{prefix} [CDN ✓] {game.name}") continue failed += 1 print(f"{prefix} [miss ] {game.name}") print(f"\n Done — {ok} downloaded, {skipped} cached, {failed} not found.") # ────────────────────────────────────────────────────────────── # Sunshine config # ────────────────────────────────────────────────────────────── def find_existing_sunshine_config() -> Optional[Path]: candidates = [ Path(os.environ.get("ProgramFiles", "C:/Program Files")) / "Sunshine/config/apps.json", Path(os.environ.get("APPDATA", "")) / "Sunshine/config/apps.json", Path(os.environ.get("LOCALAPPDATA", "")) / "Sunshine/config/apps.json", Path("C:/Program Files/Sunshine/config/apps.json"), ] for c in candidates: if c.exists(): return c return None def build_sunshine_config(games: list[Game]) -> dict: apps, seen = [], set() for g in sorted(games, key=lambda x: x.name.lower()): key = g.name.lower().strip() if key in seen: continue seen.add(key) apps.append(g.to_sunshine_entry()) return {"apps": apps, "env": {"PATH": "$(PATH):$(HOME)/.local/bin"}} # ────────────────────────────────────────────────────────────── # Main # ────────────────────────────────────────────────────────────── def main(): parser = argparse.ArgumentParser( description="Scan installed games and generate a Sunshine apps.json", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__, ) parser.add_argument("--api-key", metavar="KEY", help="SteamGridDB API key (saved for future runs)") parser.add_argument("--no-art", action="store_true", help="Skip artwork fetching entirely") parser.add_argument("--dry-run", action="store_true", help="Print the JSON without writing to disk") parser.add_argument("--out", metavar="PATH", help="Output path (default: sunshine_apps.json next to this script)") parser.add_argument("--merge", action="store_true", help="Merge with existing Sunshine apps.json (keeps manual entries)") parser.add_argument("--sources", metavar="SOURCE", nargs="+", choices=["steam", "epic", "gog", "xbox"], default=["steam", "epic", "gog", "xbox"], help="Which stores to scan (default: all)") args = parser.parse_args() games: list[Game] = [] if "steam" in args.sources: print("[*] Scanning Steam...") steam_root = find_steam_root() if steam_root: found = scan_steam(steam_root) print(f" Found {len(found)} Steam game(s) [root: {steam_root}]") games.extend(found) else: print(" Steam not found — skipping.") if "epic" in args.sources: print("[*] Scanning Epic Games...") found = scan_epic() print(f" Found {len(found)} Epic game(s)") games.extend(found) if "gog" in args.sources: print("[*] Scanning GOG...") found = scan_gog() print(f" Found {len(found)} GOG game(s)") games.extend(found) if "xbox" in args.sources: print("[*] Scanning Xbox / Microsoft Store...") found = scan_xbox() print(f" Found {len(found)} Xbox/MS game(s)") games.extend(found) print(f"\n[+] Total: {len(games)} game(s) found") if not games: print("[!] No games found. Check that your launchers are installed.") sys.exit(0) # ── Artwork ─────────────────────────────────────────────── if not args.no_art: api_key = load_api_key(args.api_key) if api_key: fetch_artwork(games, api_key) else: print("\n[!] No API key — skipping artwork. Use --no-art to suppress this message.") else: print("[*] Artwork skipped (--no-art).") # ── Build config ────────────────────────────────────────── config = build_sunshine_config(games) if args.merge: existing_path = find_existing_sunshine_config() if existing_path: try: existing = json.loads(existing_path.read_text(encoding="utf-8")) manual_entries = [a for a in existing.get("apps", []) if a["name"].lower() not in {g.name.lower() for g in games}] config["apps"] = manual_entries + config["apps"] print(f"\n[+] Merged {len(manual_entries)} existing manual entry/entries.") except Exception as e: print(f"\n[!] Could not read existing config: {e}") json_out = json.dumps(config, indent=2, ensure_ascii=False) if args.dry_run: print(json_out) print("\n[dry-run] Nothing written.") return # ── Write output ────────────────────────────────────────── out_path = Path(args.out) if args.out else Path(__file__).parent / "sunshine_apps.json" out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(json_out, encoding="utf-8") print(f"\n[✓] Written to: {out_path}") print(f" {len(config['apps'])} app entries total.") # ── Auto-copy to Sunshine ───────────────────────────────── sunshine_path = find_existing_sunshine_config() if sunshine_path: backup_path = sunshine_path.with_suffix(".json.bak") try: if sunshine_path.exists(): shutil.copy2(sunshine_path, backup_path) print(f"[+] Backed up existing config → {backup_path}") except Exception as e: print(f"[!] Could not create backup: {e}") try: sunshine_path.write_text(json_out, encoding="utf-8") print(f"[✓] Copied to Sunshine config: {sunshine_path}") print("\n[!] Restart Sunshine for the new app list to take effect.") print(" Sunshine web UI: https://localhost:47990") except PermissionError: print(f"\n[!] Permission denied writing to {sunshine_path}") print(" Re-run this script as Administrator.") except Exception as e: print(f"\n[!] Could not copy to Sunshine config: {e}") else: print("\n[!] Sunshine config directory not found — could not auto-copy.") print(f" Manually place {out_path} at:") print(" C:\\Program Files\\Sunshine\\config\\apps.json") print(" Then restart Sunshine.") if __name__ == "__main__": main() bunu anlamadım, belki apple tv'ye stream etsem - mouse & kb olmasa oyuna giremesem anlarım use case ne? ben mac mini pro'ya stream ediyorum 2.5gbit switch + 2.5gbit router üzerinden cat 7 kablo ile - tv de lg c2 router: Ruijie RG-EW7200BE PRO Temmuz 3, 2026 Goering tarafından düzenlendi
MrLevie Mesaj tarihi: Temmuz 3, 2026 Mesaj tarihi: Temmuz 3, 2026 ben direkt android projeksiyon cihazina streamliyorum ondan olabilir gamepad bagli ona da. Direkman oyunu aciyor. Ugrastirmadan.
Goering Mesaj tarihi: Temmuz 3, 2026 Mesaj tarihi: Temmuz 3, 2026 he anladım, kb&mouse olmayınca... apple tv 4k'dan o yüzden oynayamıyorum aslında bir denerim belki böyleli, sağol
Goering Mesaj tarihi: Temmuz 4, 2026 Mesaj tarihi: Temmuz 4, 2026 MrLevie, 7/3/2026 10:12 PM tarihinde dedi ki: ben direkt android projeksiyon cihazina streamliyorum ondan olabilir gamepad bagli ona da. Direkman oyunu aciyor. Ugrastirmadan. hmm python script'i denedim, directory scan eder diye bekliyordum (ilaçlı oyunlar var) - yapmadı, install edilmiş şeylere bakıyor... bir de apple tv 4k'da iyi performans vermedi, mac mini devam 4k 60hz / veya 2k - 144hz stream ediyorum
MrLevie Mesaj tarihi: Temmuz 4, 2026 Mesaj tarihi: Temmuz 4, 2026 Ha evet, platformlara baktırdım sadece. XBOX epic steam (gog var mıydı tam emin degilim). Ethernet lazım ama ya cihaz ne olursa olsun bence.
Öne çıkan mesajlar