├── requirements.txt ├── deckrommsync.db ├── docs ├── deckrommsync.png └── platform_matching.png ├── .gitignore ├── classes ├── __pycache__ │ ├── RommAPIHelper.cpython-311.pyc │ ├── BackgroundWorker.cpython-311.pyc │ └── DeckRommSyncDatabase.cpython-311.pyc ├── DeckRommSyncDatabase.py ├── RommAPIHelper.py └── BackgroundWorker.py ├── config.json ├── LICENSE.md ├── templates ├── log.html ├── base.html ├── status.html └── config.html ├── README.md └── app.py /requirements.txt: -------------------------------------------------------------------------------- 1 | APScheduler==3.11.0 2 | Django==3.1 3 | Flask==3.1.0 4 | Requests==2.32.3 5 | -------------------------------------------------------------------------------- /deckrommsync.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeriBluGaming/DeckRommSync-Standalone/HEAD/deckrommsync.db -------------------------------------------------------------------------------- /docs/deckrommsync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeriBluGaming/DeckRommSync-Standalone/HEAD/docs/deckrommsync.png -------------------------------------------------------------------------------- /docs/platform_matching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeriBluGaming/DeckRommSync-Standalone/HEAD/docs/platform_matching.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | system.log 2 | .vscode/settings.json 3 | background_worker.log 4 | test.py 5 | tests/* 6 | output/* 7 | TEST_background_worker.log 8 | -------------------------------------------------------------------------------- /classes/__pycache__/RommAPIHelper.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeriBluGaming/DeckRommSync-Standalone/HEAD/classes/__pycache__/RommAPIHelper.cpython-311.pyc -------------------------------------------------------------------------------- /classes/__pycache__/BackgroundWorker.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeriBluGaming/DeckRommSync-Standalone/HEAD/classes/__pycache__/BackgroundWorker.cpython-311.pyc -------------------------------------------------------------------------------- /classes/__pycache__/DeckRommSyncDatabase.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeriBluGaming/DeckRommSync-Standalone/HEAD/classes/__pycache__/DeckRommSyncDatabase.cpython-311.pyc -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "host": "0.0.0.0", 4 | "port": 5000 5 | }, 6 | "database": { 7 | "name": "deckrommsync.db", 8 | "type": "sqlite" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License (Modified - No Selling) 2 | 3 | Copyright (c) 2025 PeriBluGaming 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, and sublicense the Software, 9 | subject to the following conditions: 10 | 11 | 1. The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 2. The Software and any derivative works may **not** be sold or commercially distributed 14 | for monetary gain. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /templates/log.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 10 |
11 |
12 |

Background Worker Logging

13 | {% for group in log_groups %} 14 |
15 |
16 |

{{ group[0].split('- INFO')[0] }}

17 |
18 | {% for line in group %} 19 |
{{ line }}
20 | {% endfor %} 21 | 32 |
33 |
34 |
35 | {% endfor %} 36 |
37 |
38 | {% endblock content %} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeckRommSync - Standalone 2 | DeckRomMSync - Standalone is a project that automatically synchronizes your ROMs from [RomM](https://github.com/rommapp/romm) to your Steam Deck. 3 | The games are automatically copied to the correct directory. Retrodeck is required! 4 | ![DeckRomMSync](/docs/deckrommsync.png) 5 | 6 | ## Installation 7 | 1. Clone the Repository to your Steamdeck 8 | ```bash 9 | git clone https://github.com/PeriBluGaming/DeckRommSync-Standalone.git 10 | ``` 11 | 12 | 2. Create a virtual environment and activate them 13 | ```bash 14 | python -m venv venv 15 | source venv/bin/activate 16 | ``` 17 | 18 | 3. Install Requirements 19 | ```bash 20 | pip install -r requirements.txt 21 | ``` 22 | 23 | 4. (Optional) Adjust the port in the config.json file. By default, the application runs on port 5000. 24 | 25 | ## Configuration 26 | Now the installation is complete and you can start the application with following command. Make sure you have activate the environment, see Installation Point 2. 27 | ```bash 28 | python3 app.py 29 | ``` 30 | 31 | After starting the application open a Browser `http://{ip-steamdeck}:5000` and click on `Config`. 32 | Now you have to setup following Settings: 33 | 34 | ### RomM API Settings 35 | **RomM API URL:** API URL from RomM `http://{ip-romm}:{port-romm}/api`\ 36 | **Username:** your Username of RomM\ 37 | **Password:** your Password of RomM 38 | 39 | Press Save after entering the data. Then wait 2-3 minutes until the background worker has completed one cycle. Now refresh the Browser (F5) 40 | (The Background Worker will fetch your Collections / Platforms / etc.) 41 | 42 | ### Configurate Platform Matching 43 | After the Background Worker runs for the first time, you can see your Platforms and Collections on the Config-Page.\ 44 | **Steamdeck System Path:** Enter the path of your RetroDeck installation under `Steamdeck System Path`. 45 | 46 | Below, you will see a table with all platforms. 47 | For each platform, enter the folder name of your RetroDeck platform. For example: `Playstation 1 -> psx` and press Save. 48 | **Note: You must press Save for every Platform your set!!!** 49 | ![Platform-Matching](/docs/platform_matching.png) 50 | 51 | ### Activate Collection Sync 52 | To automatically sync the ROMs of one or more collections, you need to enable the collection. 53 | 54 | On the Config page, you'll find the "Sync Collections" section. Here, your collections from RomM are displayed. You can enable/disable synchronization using the checkboxes. 55 | 56 | **Note: Click Save after enabling it.** 57 | 58 | After that, the synchronization will automatic run! -------------------------------------------------------------------------------- /classes/DeckRommSyncDatabase.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from typing import List, Tuple, Any 3 | 4 | class DeckRommSyncDatabase: 5 | def __init__(self, db_name: str): 6 | """ 7 | Initialisiert die Verbindung zur SQLite-Datenbank. 8 | """ 9 | self.db_name = db_name 10 | self.connection = sqlite3.connect(self.db_name, check_same_thread=False) 11 | self.cursor = self.connection.cursor() 12 | 13 | def execute_query(self, query: str, params: Tuple = ()) -> None: 14 | """ 15 | Führt eine SQL-Abfrage ohne Rückgabewert aus (INSERT, UPDATE, DELETE). 16 | """ 17 | try: 18 | self.cursor.execute(query, params) 19 | self.connection.commit() 20 | except sqlite3.Error as e: 21 | print(f"SQLite Fehler: (0) {e}") 22 | 23 | def insert(self, table: str, columns: List[str], values: Tuple) -> None: 24 | """ 25 | Führt einen INSERT in die Datenbank aus. 26 | """ 27 | cols = ', '.join(columns) 28 | placeholders = ', '.join(['?' for _ in columns]) 29 | query = f"INSERT INTO {table} ({cols}) VALUES ({placeholders})" 30 | # print(query) 31 | self.execute_query(query, values) 32 | 33 | def update(self, table: str, updates: dict, condition: str, condition_values: Tuple) -> None: 34 | """ 35 | Führt ein UPDATE in der Datenbank aus. 36 | """ 37 | set_clause = ', '.join([f"{key} = ?" for key in updates.keys()]) 38 | query = f"UPDATE {table} SET {set_clause} WHERE {condition}" 39 | values = tuple(updates.values()) + condition_values 40 | self.execute_query(query, values) 41 | 42 | def fetch_query(self, query: str, params: Tuple = ()) -> List[Tuple]: 43 | """ 44 | Führt eine SELECT-Abfrage aus und gibt die Ergebnisse zurück. 45 | """ 46 | try: 47 | self.cursor.execute(query, params) 48 | return self.cursor.fetchall() 49 | except sqlite3.Error as e: 50 | print(f"SQLite Fehler: (1) {e}") 51 | return [] 52 | 53 | def select(self, table: str, columns: List[str] = ['*'], condition: str = '', condition_values: Tuple = ()) -> List[Tuple]: 54 | """ 55 | Führt ein SELECT in der Datenbank aus und gibt die Ergebnisse zurück. 56 | """ 57 | cols = ', '.join(columns) 58 | query = f"SELECT {cols} FROM {table}" 59 | if condition: 60 | query += f" WHERE {condition}" 61 | return self.fetch_query(query, condition_values) 62 | 63 | def select_as_dict(self, table: str, columns: List[str] = ['*'], condition: str = '', condition_values: Tuple = ()) -> List[dict]: 64 | """ 65 | Führt ein SELECT in der Datenbank aus und gibt die Ergebnisse als Liste von Dictionaries zurück. 66 | """ 67 | cols = ', '.join(columns) 68 | query = f"SELECT {cols} FROM {table}" 69 | if condition: 70 | query += f" WHERE {condition}" 71 | 72 | try: 73 | self.cursor.execute(query, condition_values) 74 | rows = self.cursor.fetchall() 75 | column_names = [desc[0] for desc in self.cursor.description] # Holt die Spaltennamen 76 | return [dict(zip(column_names, row)) for row in rows] # Erstellt Dicts 77 | except sqlite3.Error as e: 78 | print(f"SQLite Fehler: (2) {e}") 79 | return [] -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}DeckRommSync{% endblock title %} 7 | 8 | 9 | 10 | 57 | 58 | 59 | 60 | 61 | 81 | 82 | 83 |
84 | 85 | {% block content %} 86 | {% endblock content %} 87 | 88 |
89 | 90 | 91 | -------------------------------------------------------------------------------- /classes/RommAPIHelper.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from base64 import b64encode 3 | import os 4 | import urllib.parse 5 | 6 | class RommAPIHelper: 7 | def __init__(self, api_base_url): 8 | self.api_base_url = api_base_url 9 | 10 | def login(self, username, password): 11 | url = self.api_base_url + '/token' 12 | 13 | auth_string = f"{username}:{password}" 14 | self.auth_encoded = b64encode(auth_string.encode()).decode() 15 | 16 | # Heartbeat 17 | def getRommHeartbeat(self): 18 | # Prepare URL 19 | url = self.api_base_url + '/heartbeat' 20 | 21 | # Prepare Headers 22 | headers = { 23 | "accept": "application/json", 24 | "Content-Type": "application/x-www-form-urlencoded", 25 | "Authorization": f"Basic {self.auth_encoded}" 26 | } 27 | 28 | # Do HTTP GET Request 29 | response = requests.get(url, headers=headers) 30 | 31 | if response.status_code == 200: 32 | return response.json() 33 | else: 34 | print("Fehler:", response.status_code, response.text) 35 | 36 | 37 | def getCollections(self): 38 | 39 | # Prepare URL 40 | url = self.api_base_url + '/collections/' 41 | 42 | # Prepare Headers 43 | headers = { 44 | "accept": "application/json", 45 | "Content-Type": "application/x-www-form-urlencoded", 46 | "Authorization": f"Basic {self.auth_encoded}" 47 | } 48 | 49 | # Do HTTP GET Request 50 | response = requests.get(url, headers=headers) 51 | 52 | if response.status_code == 200: 53 | # print(response.text) 54 | return response.json() 55 | else: 56 | print("Fehler:", response.status_code, response.text) 57 | 58 | def getCollectionByID(self, collectionID): 59 | 60 | # Prepare URL 61 | url = self.api_base_url + '/collections/' + str(collectionID) 62 | 63 | # Prepare Headers 64 | headers = { 65 | "accept": "application/json", 66 | "Content-Type": "application/x-www-form-urlencoded", 67 | "Authorization": f"Basic {self.auth_encoded}" 68 | } 69 | 70 | # Do HTTP GET Request 71 | response = requests.get(url, headers=headers) 72 | 73 | if response.status_code == 200: 74 | # print(response.text) 75 | return response.json() 76 | else: 77 | print("Fehler:", response.status_code, response.text) 78 | 79 | 80 | def getPlatforms(self): 81 | # Prepare URL 82 | url = self.api_base_url + '/platforms/' 83 | 84 | # Prepare Headers 85 | headers = { 86 | "accept": "application/json", 87 | "Content-Type": "application/x-www-form-urlencoded", 88 | "Authorization": f"Basic {self.auth_encoded}" 89 | } 90 | 91 | # Do HTTP GET Request 92 | response = requests.get(url, headers=headers) 93 | 94 | if response.status_code == 200: 95 | # print(response.text) 96 | return response.json() 97 | else: 98 | print("Fehler:", response.status_code, response.text) 99 | 100 | def getRomByID(self, romID): 101 | # Prepare URL 102 | url = self.api_base_url + '/roms/' + str(romID) 103 | 104 | # Prepare Headers 105 | headers = { 106 | "accept": "application/json", 107 | "Content-Type": "application/x-www-form-urlencoded", 108 | "Authorization": f"Basic {self.auth_encoded}" 109 | } 110 | 111 | # Do HTTP GET Request 112 | response = requests.get(url, headers=headers) 113 | 114 | if response.status_code == 200: 115 | # print(response.text) 116 | return response.json() 117 | else: 118 | print("Fehler:", response.status_code, response.text) 119 | 120 | def downloadRom(self, romID, romFilename, download_path): 121 | # Prepare URL 122 | url = self.api_base_url + '/roms/' + str(romID) + '/content/' + str(romFilename) 123 | 124 | # Prepare Headers 125 | headers = { 126 | "accept": "application/json", 127 | "Content-Type": "application/x-www-form-urlencoded", 128 | "Authorization": f"Basic {self.auth_encoded}" 129 | } 130 | 131 | # Do HTTP GET Request 132 | response = requests.get(url, headers=headers) 133 | 134 | if response.status_code == 200: 135 | # Get Filename from HTTP-Request Response 136 | content_disposition = response.headers.get("content-disposition") 137 | if content_disposition and "filename=" in content_disposition: 138 | filename = content_disposition.split("filename=")[1].strip('"') 139 | filename = urllib.parse.unquote(filename) # Dekodiert %20 zu Leerzeichen 140 | else: 141 | filename = romFilename 142 | 143 | # make sure the Download Folder exists | If not, create it 144 | os.makedirs(download_path, exist_ok=True) 145 | 146 | # build file-path 147 | file_path = os.path.join(download_path, filename) 148 | 149 | # Check if File exists 150 | if os.path.exists(file_path): 151 | print(f"⚠️ File already exists: {file_path} – Download übersprungen.") 152 | else: 153 | # Download File in Chunks and save it 154 | with open(file_path, "wb") as file: 155 | for chunk in response.iter_content(chunk_size=8192): 156 | file.write(chunk) 157 | else: 158 | # Something wrong 159 | print("Error:", response.status_code, response.text) 160 | -------------------------------------------------------------------------------- /classes/BackgroundWorker.py: -------------------------------------------------------------------------------- 1 | from classes.RommAPIHelper import RommAPIHelper 2 | from classes.DeckRommSyncDatabase import DeckRommSyncDatabase 3 | import json 4 | import logging 5 | 6 | class BackgroundWorker: 7 | def __init__(self, dbName, logger): 8 | self.background_logger = logger 9 | self.dbName = dbName 10 | db = DeckRommSyncDatabase(dbName) 11 | configObj = db.select_as_dict("config") 12 | for config in configObj: 13 | if config['config_key'] == 'romm_api_base_url': 14 | self.romMAPIBaseUrl = config['config_value'] 15 | elif config['config_key'] == 'romm_username': 16 | self.romMUsername = config['config_value'] 17 | elif config['config_key'] == 'romm_password': 18 | self.romMPassword = config['config_value'] 19 | 20 | def sync_rommCollections(self): 21 | # Write Log 22 | self.background_logger.info("Syncing RomM Collections - Starting") 23 | romm = RommAPIHelper(self.romMAPIBaseUrl) 24 | romm.login(self.romMUsername, self.romMPassword) 25 | db = DeckRommSyncDatabase(self.dbName) 26 | 27 | # Read Platforms from RomM 28 | platform_result = romm.getPlatforms() 29 | for platform in platform_result: 30 | # Insert Platforms 31 | db.insert("platforms_matching", ["romm_platform_id", "romm_platform_name"], (platform['id'], platform['name'])) 32 | 33 | # Read Collections from RomM 34 | collection_result = romm.getCollections() 35 | 36 | # Go Through Collections and Insert them into the Database 37 | for collection in collection_result: 38 | if isinstance(collection['path_covers_large'], list) and collection['path_covers_large']: 39 | first_cover = collection['path_covers_large'][0] # Sicher das erste Element holen 40 | else: 41 | first_cover = collection['path_covers_large'] # Falls es kein Array ist, einfach den Wert übernehmen 42 | 43 | db.insert("collections", ["collections_id", "name", "rom_count", "cover", "collection_sync"], 44 | (collection['id'], 45 | collection['name'], 46 | collection['rom_count'], 47 | first_cover, 48 | 0)) 49 | 50 | # Read ROMs from Collection 51 | roms = collection['rom_ids'] 52 | for rom in roms: 53 | romObj = romm.getRomByID(rom) 54 | filename = romObj['fs_name'] 55 | 56 | # Insert ROM 57 | db.insert("roms", ["roms_id", "collections_id", "name", "url_cover", "filename", "platform_fs_slug", "platform_id"], 58 | (romObj['id'], 59 | collection['id'], 60 | romObj['name'], 61 | romObj['url_cover'], 62 | filename, 63 | romObj['platform_fs_slug'], 64 | romObj['platform_id'])) 65 | # Write Log 66 | self.background_logger.info("Syncing RomM Collections - Finished") 67 | 68 | 69 | def sync_copyRoms(self): 70 | # Write Log 71 | self.background_logger.info("Syncing ROMS - Starting") 72 | db = DeckRommSyncDatabase(self.dbName) 73 | 74 | # Get Collections to sync 75 | collections = db.select_as_dict("collections", ['*'], 'collection_sync = ?', (1,)) 76 | 77 | # Get Steamdeck Path 78 | steamdeck_path = db.select_as_dict("config", ['config_value'], 'config_key = ?', ('steamdeck_retrodeck_path',)) 79 | steamdeck_path = steamdeck_path[0].get("config_value") 80 | 81 | # DEBUG: Set Steamdeck Path manually 82 | # steamdeck_path = "./output/" 83 | 84 | for collection in collections: 85 | 86 | self.background_logger.info(f"Check Collection: {collection.get('name')}") 87 | 88 | # Create RomM API Helper 89 | romm = RommAPIHelper(self.romMAPIBaseUrl) 90 | romm.login(self.romMUsername, self.romMPassword) 91 | 92 | # Get Roms from Collection 93 | roms = db.select_as_dict("roms", ['*'], 'collections_id = ? and sync_status = ?', (collection.get("collections_id"), 0)) 94 | for rom in roms: 95 | roms_id = rom.get("roms_id") 96 | filename = rom.get("filename") 97 | platform_id = rom.get("platform_id") 98 | 99 | # Get Rom-Matching 100 | platform_matching = db.select_as_dict("platforms_matching", ['*'], 'romm_platform_id = ?', (platform_id,)) 101 | steamdeck_platform_path = platform_matching[0].get("steamdeck_platform_name") 102 | 103 | # Debug Print 104 | # print(f"ROMS-ID: {roms_id}") 105 | # print(f"Filename: {filename}") 106 | # print(f"Platform-ID: {platform_id}") 107 | # print(f"Copy {filename} to {steamdeck_path}{steamdeck_platform_path}/{filename}") 108 | # print("--------------------") 109 | 110 | self.background_logger.info(f"ROMS-ID: {roms_id} | Copy {filename} to {steamdeck_path}{steamdeck_platform_path}/{filename}") 111 | 112 | # Download Rom 113 | romm.downloadRom(roms_id, filename, steamdeck_path + steamdeck_platform_path + "/") 114 | 115 | self.background_logger.info(f"ROM-ID: {roms_id} | Copy Finished") 116 | 117 | # Update Sync Status 118 | db.update("roms", {"sync_status": "1"}, "roms_id = ?", (roms_id,)) 119 | # Write Log 120 | self.background_logger.info("Syncing ROMS - Finished") -------------------------------------------------------------------------------- /templates/status.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 16 |
17 |
18 |

Collections

19 | {% for collection in collections %} 20 | 21 |
22 |
23 |
24 |
25 |
26 | Placeholder image 28 |
29 |
30 |
31 |

{{ collection.name }}

32 |

{{ collection.roms|length }} Roms

33 |
34 | 35 |
36 |
37 | 38 |
39 | {% for rom in collection.roms %} 40 |
41 |
42 |
43 | Placeholder image 44 |
45 |
46 |
47 |
{{ rom.name }}
48 |
{{ rom.platform_fs_slug }}
49 |
50 | {% if rom.sync_status == 0 %} 51 | 52 | {% elif rom.sync_status == 1 %} 53 | 54 | {% elif rom.sync_status == 2 %} 55 | 56 | {% endif %} 57 | 78 |
79 | {% endfor %} 80 |
81 | 82 |
83 |
84 |
85 | {% endfor %} 86 |
87 |
88 | 89 | 134 | {% endblock content %} 135 | -------------------------------------------------------------------------------- /templates/config.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |

RomM API Settings

7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 | 15 |
16 |
17 | 18 |
19 | 20 |
21 | 22 |
23 |
24 | 25 |
26 | 27 |
28 | 29 |
30 |
31 | 32 |
33 | 36 |
37 |
38 |
39 |
40 | 41 |

Sync Collections

42 |
43 |
44 | 45 |
46 |
47 |
48 | {% for collection in collections %} 49 |
50 | 51 | 52 |
53 |
{{ collection.name }}
54 |
55 |
56 |
57 | {% endfor %} 58 |
59 |
60 | 61 |
62 | 65 |
66 |
67 |
68 |
69 | 70 |

Platform Matching

71 |
72 |
73 | 74 |
75 | 76 |
77 |
78 |
79 | 80 |
81 |
82 | 85 |
86 |
87 |
88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | {% for platform in platform_matching %} 101 | 102 | 103 | 104 | 105 | 115 | 120 | 121 | 122 | {% endfor %} 123 | 124 |
IDRomM Platform NamePlatform Folder SteamdeckAction
{{ platform.romm_platform_id }}{{ platform.romm_platform_name }} 106 | 112 | 113 | 114 | 116 | 119 |
125 |
126 |
127 | 128 | 129 | 140 |
141 |
142 | {% endblock content %} -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, render_template, request, jsonify, url_for 2 | from django.shortcuts import render 3 | from apscheduler.schedulers.background import BackgroundScheduler 4 | from classes.RommAPIHelper import RommAPIHelper 5 | from classes.DeckRommSyncDatabase import DeckRommSyncDatabase 6 | from classes.BackgroundWorker import BackgroundWorker 7 | import json 8 | import os 9 | import logging 10 | 11 | # Logging für den Background Worker 12 | background_logger = logging.getLogger("background_worker") 13 | background_logger.setLevel(logging.INFO) 14 | background_handler = logging.FileHandler("background_worker.log", encoding="utf-8") 15 | background_formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S") 16 | background_handler.setFormatter(background_formatter) 17 | background_logger.addHandler(background_handler) 18 | 19 | app = Flask(__name__) 20 | 21 | def run_background_task(): 22 | """Ruft die `run()`-Methode der Hintergrundklasse auf.""" 23 | # Background Worker erstellen 24 | bgWorker = BackgroundWorker("deckrommsync.db", background_logger) 25 | background_logger.info("Background Task started...") 26 | bgWorker.sync_rommCollections() 27 | bgWorker.sync_copyRoms() 28 | background_logger.info("Background Task finished...") 29 | 30 | 31 | # System-Logger einrichten 32 | system_logger = logging.getLogger("system_logger") 33 | system_logger.setLevel(logging.INFO) 34 | system_handler = logging.FileHandler("system.log", encoding="utf-8") 35 | system_formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") 36 | system_handler.setFormatter(system_formatter) 37 | system_logger.addHandler(system_handler) 38 | 39 | def load_json_config(file_path="config.json"): 40 | """Lädt die Konfiguration aus der JSON-Datei.""" 41 | system_logger.info("Load Config from File") 42 | if os.path.exists(file_path): 43 | with open(file_path, "r") as f: 44 | return json.load(f) 45 | return {} 46 | 47 | @app.route('/') 48 | def status(): 49 | system_logger.info("Status Page") 50 | db = DeckRommSyncDatabase(app_config["database"].get("name", "deckrommsync.db")) 51 | collection_db_result = db.select_as_dict("collections", ['*'], 'collection_sync = 1') 52 | collections = [] 53 | for collection in collection_db_result: 54 | roms_in_collection = db.select_as_dict("roms", ['*'], 'collections_id = ?', (collection["collections_id"],)) 55 | collection["roms"] = roms_in_collection 56 | collections.append(collection) 57 | 58 | return render_template('status.html', status="Server läuft", version="1.0.0", collections=collections) 59 | 60 | @app.route('/config', methods=['GET', 'POST']) 61 | def config(): 62 | db = DeckRommSyncDatabase(app_config["database"].get("name", "deckrommsync.db")) 63 | # Hole Config 64 | config_result = db.select("config") 65 | config_dict = {row[1]: row[2] for row in config_result} # Wandelt die Liste in ein Dictionary um 66 | 67 | # Hole Platform Matching 68 | platform_matching = db.select_as_dict("platforms_matching") 69 | 70 | # Hole Collections 71 | collections = db.select_as_dict("collections") 72 | 73 | if request.method == 'POST': 74 | new_config = request.form.to_dict() 75 | # Save Config 76 | return render_template('config.html', config=config_dict, collections=collections, platform_matching=platform_matching) 77 | 78 | # Update Romm API Settings 79 | @app.route('/config/config_romm_api_settings', methods=['POST']) 80 | def config_romm_api_settings(): 81 | 82 | # Create Database Object 83 | db = DeckRommSyncDatabase(app_config["database"].get("name", "deckrommsync.db")) 84 | 85 | # Update Config in Database 86 | db.update("config", {"config_value": request.form.get("romm_api_base_url")}, "config_key = ?", ("romm_api_base_url",)) 87 | db.update("config", {"config_value": request.form.get("romm_username")}, "config_key = ?", ("romm_username",)) 88 | db.update("config", {"config_value": request.form.get("romm_password")}, "config_key = ?", ("romm_password",)) 89 | 90 | return redirect(url_for('config')) 91 | 92 | # Update Collection Sync Settings 93 | @app.route('/config/config_collection_sync_settings', methods=['POST']) 94 | def config_collection_sync_settings(): 95 | 96 | # Create Database Object 97 | db = DeckRommSyncDatabase(app_config["database"].get("name", "deckrommsync.db")) 98 | 99 | # Get Collections IDs from Form 100 | collections_ids = request.form.getlist("collections_id") 101 | 102 | for collections_id in collections_ids: 103 | # Get Checkbox Value 104 | checkbox_value = "1" if f"collection_sync_{collections_id}" in request.form else "0" 105 | db.update("collections", {"collection_sync": checkbox_value}, "collections_id = ?", (collections_id,)) 106 | 107 | return redirect(url_for('config')) 108 | 109 | # Update Platform Matching 110 | @app.route('/config/config_platform_matching', methods=['POST']) 111 | def config_platform_matching(): 112 | 113 | # Create Database Object 114 | db = DeckRommSyncDatabase(app_config["database"].get("name", "deckrommsync.db")) 115 | 116 | # Update Config in Database 117 | db.update("platforms_matching", {"steamdeck_platform_name": request.form.get("steamdeck_platform_name")}, "romm_platform_id = ?", (request.form.get("romm_platform_id"),)) 118 | 119 | return redirect(url_for('config')) 120 | 121 | # Update Steamdeck Platform Path 122 | @app.route('/config/config_steamdeck_platform_path', methods=['POST']) 123 | def config_steamdeck_platform_path(): 124 | 125 | # Create Database Object 126 | db = DeckRommSyncDatabase(app_config["database"].get("name", "deckrommsync.db")) 127 | 128 | # Update Config in Database 129 | db.update("config", {"config_value": request.form.get("steamdeck_path")}, "config_key = ?", ("steamdeck_retrodeck_path",)) 130 | 131 | return redirect(url_for('config')) 132 | 133 | # Status Dropdown: Reset Status 134 | @app.route('/dropdown/reset_status', methods=['POST']) 135 | def dropdown_reset_status(): 136 | data = request.get_json() 137 | 138 | # Create Database Object 139 | db = DeckRommSyncDatabase(app_config["database"].get("name", "deckrommsync.db")) 140 | 141 | # Update Rom Status 142 | db.update("roms", {"sync_status": "0"}, "roms_id = ?", (data['roms_id'],)) 143 | 144 | return jsonify({"message": "Daten erfolgreich empfangen!", "data": data}) 145 | 146 | @app.route('/log') 147 | def log(): 148 | """Liest die Log-Datei zeilenweise ein und gibt eine Liste zurück.""" 149 | """Liest die Log-Datei und teilt sie in Abschnitte, wenn 'Background Task finished...' vorkommt.""" 150 | try: 151 | with open("background_worker.log", "r", encoding="utf-8") as file: 152 | logs = [] 153 | current_section = [] 154 | 155 | for line in file: 156 | if "Background Task started..." in line: 157 | if current_section: 158 | logs.append(current_section) # Speichere die aktuelle Gruppe 159 | current_section = [] # Starte eine neue Gruppe 160 | current_section.append(line.strip()) # Füge Zeile zur aktuellen Gruppe hinzu 161 | 162 | if current_section: # Letzte Gruppe hinzufügen (falls vorhanden) 163 | logs.append(current_section) 164 | 165 | log_content = logs[::-1] # Neueste Gruppe zuerst 166 | except FileNotFoundError: 167 | return [["Log-Datei nicht gefunden!"]] 168 | 169 | return render_template('log.html', log_groups=log_content) 170 | 171 | if __name__ == '__main__': 172 | system_logger.info("Flask-App started...") 173 | # Config 174 | global app_config 175 | app_config = load_json_config() 176 | 177 | # DEBUG 178 | # Scheduler starten 179 | scheduler = BackgroundScheduler() 180 | scheduler.add_job(run_background_task, "interval", minutes=1) # Alle 2 Minuten 181 | scheduler.start() 182 | 183 | app.run(debug=True, use_reloader=False, host=app_config["server"].get("host", "localhost"), port=app_config["server"].get("port", 5000)) 184 | --------------------------------------------------------------------------------