134 lines
4.2 KiB
Python
134 lines
4.2 KiB
Python
# source.py
|
|
#
|
|
# Copyright 2023 Geoffrey Coulaud
|
|
#
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
import sys
|
|
from abc import abstractmethod
|
|
from collections.abc import Iterable
|
|
from typing import Any, Collection, Generator, Optional
|
|
|
|
from cartridges.game import Game
|
|
from cartridges.importer.location import Location, UnresolvableLocationError
|
|
|
|
# Type of the data returned by iterating on a Source
|
|
SourceIterationResult = Optional[Game | tuple[Game, tuple[Any]]]
|
|
|
|
|
|
class SourceIterable(Iterable):
|
|
"""Data producer for a source of games"""
|
|
|
|
source: "Source"
|
|
|
|
def __init__(self, source: "Source") -> None:
|
|
self.source = source
|
|
|
|
@abstractmethod
|
|
def __iter__(self) -> Generator[SourceIterationResult, None, None]:
|
|
"""
|
|
Method that returns a generator that produces games
|
|
* Should be implemented as a generator method
|
|
* May yield `None` when an iteration hasn't produced a game
|
|
* In charge of handling per-game errors
|
|
* Returns when exhausted
|
|
"""
|
|
|
|
|
|
class Source(Iterable):
|
|
"""Source of games. E.g an installed app with a config file that lists game directories"""
|
|
|
|
source_id: str
|
|
name: str
|
|
variant: Optional[str] = None
|
|
available_on: set[str] = set()
|
|
iterable_class: type[SourceIterable]
|
|
|
|
# NOTE: Locations must be set at __init__ time, not in the class definition.
|
|
# They must not be shared between source instances.
|
|
locations: Collection[Location]
|
|
|
|
@property
|
|
def full_name(self) -> str:
|
|
"""The source's full name"""
|
|
full_name_ = self.name
|
|
if self.variant:
|
|
full_name_ += f" ({self.variant})"
|
|
return full_name_
|
|
|
|
@property
|
|
def game_id_format(self) -> str:
|
|
"""The string format used to construct game IDs"""
|
|
return self.source_id + "_{game_id}"
|
|
|
|
@property
|
|
def is_available(self) -> bool:
|
|
return sys.platform in self.available_on
|
|
|
|
def make_executable(self, *args, **kwargs) -> str:
|
|
"""
|
|
Create a game executable command.
|
|
Should be implemented by child classes.
|
|
"""
|
|
|
|
def __iter__(self) -> Generator[SourceIterationResult, None, None]:
|
|
"""
|
|
Get an iterator for the source
|
|
:raises UnresolvableLocationError: Not iterable
|
|
if any of the mandatory locations are unresolvable
|
|
"""
|
|
for location in self.locations:
|
|
try:
|
|
location.resolve()
|
|
except UnresolvableLocationError as error:
|
|
if not error.optional:
|
|
raise UnresolvableLocationError from error
|
|
return iter(self.iterable_class(self))
|
|
|
|
|
|
class ExecutableFormatSource(Source):
|
|
"""Source class that uses a simple executable format to start games"""
|
|
|
|
@property
|
|
@abstractmethod
|
|
def executable_format(self) -> str:
|
|
"""The executable format used to construct game executables"""
|
|
|
|
def make_executable(self, *args, **kwargs) -> str:
|
|
"""Use the executable format to"""
|
|
return self.executable_format.format(*args, **kwargs)
|
|
|
|
|
|
# pylint: disable=abstract-method
|
|
class URLExecutableSource(ExecutableFormatSource):
|
|
"""Source class that use custom URLs to start games"""
|
|
|
|
url_format: str
|
|
|
|
@property
|
|
def executable_format(self) -> str:
|
|
match sys.platform:
|
|
case "win32":
|
|
return "start " + self.url_format
|
|
case "linux":
|
|
return "xdg-open " + self.url_format
|
|
case "darwin":
|
|
return "open " + self.url_format
|
|
case other:
|
|
raise NotImplementedError(
|
|
f"No URL handler command available for {other}"
|
|
)
|