gsgbackup/sgbackup/steam.py

353 lines
12 KiB
Python

###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 Christian Moser #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
import os
import re
from pathlib import Path
import sys
import json
from .settings import settings
from .game import STEAM_GAMES,STEAM_WINDOWS_GAMES,STEAM_LINUX_GAMES,STEAM_MACOS_GAMES
PLATFORM_WINDOWS = (sys.platform.lower() == 'win32')
PLATFORM_LINUX = (sys.platform.lower() in ('linux','freebsd','netbsd','openbsd','dragonfly'))
PLATFORM_MACOS = (sys.platform.lower() == 'macos')
from gi.repository.GObject import GObject,Property,Signal
class AcfFileParser(object):
"""
Parses steam acf files to a dict.
"""
def __init__(self):
pass
def __parse_section(self,lines:list):
option_pattern = re.compile("^\"(.+)\"([ \\t]+)\"(.*)\"$")
section_pattern = re.compile("^\"(.+)\"")
line_count = 0
ret = {}
line_count=0
while line_count < len(lines):
line = lines[line_count]
line_count+=1
if line == '}':
break
s=line.strip()
match = option_pattern.fullmatch(line)
if match:
name=line[match.start(1):match.end(1)]
value=line[match.start(3):match.end(3)]
ret[name] = value
else:
match2 = section_pattern.fullmatch(line)
if match2:
name=line[match2.start(1):match2.end(1)]
if lines[line_count] == '{':
line_count += 1
n_lines,sect = self.__parse_section(lines[line_count:])
line_count += n_lines
ret[name]=sect
return line_count,ret
def parse_file(self,acf_file)->dict:
if not os.path.isfile(acf_file):
raise FileNotFoundError("File \"{s}\" does not exist!")
with open(acf_file,'r') as ifile:
buffer = ifile.read()
lines = [l.strip() for l in buffer.split('\n')]
if lines[0] == "\"AppState\"" and lines[1] == "{":
n_lines,sect = self.__parse_section(lines[2:])
return sect
raise RuntimeError("Not a acf file!")
class IgnoreSteamApp(GObject):
__gtype_name__ = "sgbackup-steam-IgnoreSteamApp"
def __init__(self,appid:int,name:str,reason:str):
GObject.__init__(self)
self.__appid = int(appid)
self.__name = name
self.__reason = reason
@staticmethod
def new_from_dict(conf:dict):
if ('appid' in conf and 'name' in conf):
appid = conf['appid']
name = conf['name']
reason = conf['reason'] if 'reason' in conf else ""
return SteamIgnoreApp(appid,name,reason)
return None
@Property(type=int)
def appid(self)->str:
return self.__appid
@Property(type=str)
def name(self)->str:
return self.__name
@name.setter
def name(self,name:str):
self.__name = name
@Property(type=str)
def reason(self)->str:
return self.__reason
@reason.setter
def reason(self,reason:str):
self.__reason = reason
def serialize(self):
return {
'appid': self.appid,
'name': self.name,
'reason': self.reason,
}
class SteamApp(GObject):
__gtype_name__ = "sgbackup-steam-SteamApp"
def __init__(self,appid:int,name:str,installdir:str):
GObject.__init__(self)
self.__appid = int(appid)
self.__name = name
self.__installdir = installdir
@Property(type=int)
def appid(self):
return self.__appid
@Property
def name(self):
return self.__name
@Property
def installdir(self):
return self.__installdir
def __str__(self):
return '{}: {}'.format(self.appid,self.name)
def __gt__(self,other):
return self.appid > other.appid
def __lt__(self,other):
return self.appid < other.appid
def __eq__(self,other):
return self.appid == other.appid
class SteamLibrary(GObject):
__gtype_name__ = "sgbackup-steam-SteamLibrary"
def __init__(self,library_path:str):
GObject.__init__(self)
self.directory = library_path
@Property(type=str)
def directory(self):
return self.__directory
@directory.setter
def directory(self,directory:str):
if not os.path.isabs(directory):
raise ValueError("\"directory\" is not an absolute path!")
if not os.path.isdir(directory):
raise NotADirectoryError("\"{}\" is not a directory or does not exist!".format(directory))
self.__directory = directory
@Property
def path(self)->Path:
return Path(self.directory).resolve()
@Property
def steam_apps(self)->list[SteamApp]:
parser = AcfFileParser()
appdir = self.path / "steamapps"
commondir = appdir / "common"
ret = []
for acf_file in appdir.glob('appmanifest_*.acf'):
if not acf_file.is_file():
continue
try:
data = parser.parse_file(str(acf_file))
app = SteamApp(data['appid'],data['name'],str(commondir/data['installdir']))
ret.append(app)
except:
pass
return sorted(ret)
class Steam(GObject):
__gtype_name__ = "sgbackup-steam-Steam"
def __init__(self):
GObject.__init__(self)
self.__libraries = []
self.__ignore_apps = {}
if not self.steamlib_list_file.is_file():
if (PLATFORM_WINDOWS):
libdirs=[
"C:\\Program Files (x86)\\steam",
"C:\\Program Files\\steam",
]
for i in libdirs:
if (os.path.isdir(i)):
self.__libraries.append(SteamLibrary(i))
break
else:
with open(str(self.steamlib_list_file),'r',encoding="utf-8") as ifile:
for line in (i.strip() for i in ifile.readlines()):
if not line or line.startswith('#'):
continue
libdir = Path(line).resolve()
if libdir.is_dir():
try:
self.add_library(str(libdir))
except:
pass
if self.ignore_apps_file.is_file():
with open(str(self.ignore_apps_file),'r',encoding="utf-8") as ifile:
ignore_list = json.loads(ifile.read())
for ignore in ignore_list:
try:
ignore_app = IgnoreSteamApp.new_from_dict(ignore)
except:
continue
if ignore_app:
self.__ignore_apps[ignore_app.appid] = ignore_app
#__init__()
@Property
def steamlib_list_file(self)->Path:
return Path(settings.config_dir).resolve() / 'steamlib.lst'
@Property
def ignore_apps_file(self)->Path:
return Path(settings.config_dir).resolve / 'ignore_steamapps.json'
@Property
def libraries(self)->list[SteamLibrary]:
return self.__libraries
@Property
def ignore_apps(self)->dict[int:IgnoreSteamApp]:
return self.__ignore_apps
def __write_steamlib_list_file(self):
with open(self.steamlib_list_file,'w',encoding='utf-8') as ofile:
ofile.write('\n'.join(str(sl.directory) for sl in self.libraries))
def __write_ignore_steamapps_file(self):
with open(self.ignore_apps_file,'w',encoding='utf-8') as ofile:
ofile.write(json.dumps([i.serialize() for i in self.ignore_apps.values()]))
def add_library(self,steamlib:SteamLibrary|str):
if isinstance(steamlib,SteamLibrary):
lib = steamlib
else:
lib = SteamLibrary(steamlib)
lib_exists = False
for i in self.libraries:
if i.derctory == lib.directory:
lib_exists = True
break
if not lib_exists:
self.__libraries.append(lib)
self.__write_steamlib_list_file()
def remove_library(self,steamlib:SteamLibrary|str):
if isinstance(steamlib,SteamLibrary):
libdir = steamlib.directory
else:
libdir = str(steamlib)
delete_libs=[]
for i in range(len(self.__libraries)):
if self.__libraries[i].directory == libdir:
delete_libs.append(i)
if delete_libs:
for i in sorted(delete_libs,reverse=True):
del self.__libraries[i]
self.__write_steamlib_list_file()
def add_ignore_app(self,app:IgnoreSteamApp):
self.__ignore_apps[app.appid] = app
self.__write_ignore_steamapps_file()
def remove_ignore_app(self,app:IgnoreSteamApp|int):
if isinstance(app,IgnoreSteamApp):
appid = app.appid
else:
appid = int(app)
if appid in self.__ignore_apps:
del self.__ignore_apps[appid]
self.__write_ignore_steamapps_file()
def find_new_steamapps(self)->list[SteamApp]:
new_apps = []
for lib in self.libraries:
for app in lib.steam_apps:
if not app.appid in STEAM_GAMES and not app.appid in self.ignore_apps:
new_apps.append(app)
return sorted(new_apps)
def update_steam_apps(self):
for lib in self.libraries():
for app in lib.steam_apps:
if PLATFORM_WINDOWS:
if ((app.appid in STEAM_WINDOWS_GAMES)
and (STEAM_WINDOWS_GAMES[app.appid].installdir != app.installdir)):
game = STEAM_WINDOWS_GAMES[app.appid]
game.installdir = app.installdir
game.save()
elif PLATFORM_LINUX:
if ((app.appid in STEAM_LINUX_GAMES)
and (STEAM_LINUX_GAMES[app.appid].installdir != app.installdir)):
game = STEAM_LINUX_GAMES[app.appid]
game.installdir = app.installdir
game.save()
elif PLATFORM_MACOS:
if ((app.appid in STEAM_MACOS_GAMES)
and (STEAM_MACOS_GAMES[app.appid].installdir != app.installdir)):
game = STEAM_MACOS_GAMES[app.appid]
game.installdir = app.installdir
game.save()