Skip to content

Commit 1050236

Browse files
authored
Merge pull request #23 from Garulf/improve-search
Improve Playnite search and library detection
2 parents 3fb6267 + d70c8e2 commit 1050236

File tree

9 files changed

+348
-183
lines changed

9 files changed

+348
-183
lines changed

README.md

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,6 @@ Copy and paste this into your browser to automatically start installation.
2424

2525
Simply type the default keyword: `pn` and search your Playnite library.
2626

27-
### Filter by Source:
28-
29-
Type: `#` to filter by a specific Source
30-
31-
![Screenshot 2022-01-11 103736](https://user-images.githubusercontent.com/535299/148973352-27c22827-4a19-4975-83e6-24bc814103ca.png)
32-
33-
34-
### Filter by install status:
35-
36-
Type: `@` to show all games including uninstalled games.
37-
38-
![Screenshot 2022-01-11 103608](https://user-images.githubusercontent.com/535299/148973214-aecfd4b9-20a5-4d55-a998-b6e972673187.png)
39-
4027

4128
## Screenshots:
4229
<details>

SettingsTemplate.yaml

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,34 @@ body:
77
- type: input
88
attributes:
99
name: playnite_path
10-
label: 'Playnite User Directory:'
11-
description: Location of Playnite user folder
10+
label: 'Playnite Directory:'
11+
description: Location of Playnite folder or database file.
12+
defaultValue: "%appdata%\\Playnite"
13+
- type: textBlock
14+
attributes:
15+
name: description
16+
description: >
17+
Path should be either your Playnite install directory or the database file path.
18+
Examples:
19+
- type: textBlock
20+
attributes:
21+
name: description
22+
description: >
23+
C:\Users\%USERNAME%\AppData\Roaming\Playnite
24+
- type: textBlock
25+
attributes:
26+
name: description
27+
description: >
28+
%APPDATA%\Playnite\ExtensionsData\FlowLauncher_Exporter\library.json
1229
- type: checkbox
1330
attributes:
1431
name: hide_uninstalled
1532
label: 'Hide Uninstalled:'
1633
defaultValue: "true"
1734
description: Hides uninstalled from default results
18-
- type: textBlock
35+
- type: checkbox
1936
attributes:
20-
name: description
21-
description: >
22-
Hide Games that are currently not installed from defautl results. You can unhide them temporarily by using the "@" keyword.
37+
name: show_hidden
38+
label: 'Show Hidden:'
39+
defaultValue: "false"
40+
description: Show hidden games in default results

plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"Name": "Playnite",
55
"Description": "Search and launch your Playnite library.",
66
"Author": "Garulf",
7-
"Version": "1.7.0",
7+
"Version": "2.0.0",
88
"Language": "python",
99
"Website": "https://github.com/Garulf/playnite-plugin",
1010
"IcoPath": "./icon.png",

plugin/exceptions.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
class LibraryNotFound(Exception):
3+
4+
def __init__(self, library):
5+
self.library = library
6+
7+
def __str__(self):
8+
return f"No library file not found for {self.library}"
9+
10+
class PlayniteNotFound(Exception):
11+
12+
def __init__(self, path):
13+
self.path = path
14+
15+
def __str__(self):
16+
return f"Playnite directory not found at {self.path}"

plugin/filters.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from typing import TYPE_CHECKING, List
2+
3+
if TYPE_CHECKING:
4+
from playnite import Game
5+
6+
class LibraryFilter:
7+
8+
def __init__(self, invert: bool = False):
9+
self.invert = invert
10+
11+
def filter(self, game: 'Game') -> bool:
12+
pass
13+
14+
def __call__(self, game: 'Game') -> bool:
15+
if self.invert:
16+
return not self.filter(game)
17+
return self.filter(game)
18+
19+
class IsHidden(LibraryFilter):
20+
21+
def filter(self, game: 'Game') -> bool:
22+
return game.Hidden
23+
24+
class IsInstalled(LibraryFilter):
25+
26+
def filter(self, game: 'Game') -> bool:
27+
return game.IsInstalled
28+
29+
class IsSource(LibraryFilter):
30+
31+
def __init__(self, source: str, *args, **kwargs):
32+
super().__init__(*args, **kwargs)
33+
self.source = source
34+
35+
def filter(self, game: 'Game') -> bool:
36+
if game.Source is not None:
37+
return game.Source["Name"].lower() == self.source.lower()
38+
return False
39+
40+
class IsID(LibraryFilter):
41+
42+
def __init__(self, id: str, *args, **kwargs):
43+
super().__init__(*args, **kwargs)
44+
self.id = id
45+
46+
def filter(self, game: 'Game') -> bool:
47+
return game.Id == self.id
48+
49+
def filter_game(filters: List[LibraryFilter], game: "Game", query: str = None) -> bool:
50+
for filter in filters:
51+
if not isinstance(filter, LibraryFilter):
52+
filter = filter()
53+
if not filter(game):
54+
return False
55+
return True

plugin/game.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass, field
4+
from datetime import datetime
5+
from pathlib import Path
6+
import webbrowser
7+
from typing import TypedDict, TYPE_CHECKING, Union
8+
9+
PLAYNITE_SCHEME = 'playnite://'
10+
11+
if TYPE_CHECKING:
12+
from playnite import PlayniteApp
13+
14+
class ReleaseDate(TypedDict):
15+
"""Release date of the game"""
16+
Day: int
17+
Month: int
18+
Year: int
19+
20+
class Source(TypedDict):
21+
"""Source library of the game"""
22+
Name: str
23+
Id: str
24+
25+
ID = str
26+
NAME = str
27+
PLAYTIME = int
28+
IS_INSTALLED = bool
29+
INSTALL_DIRECTORY = str
30+
ICON = str
31+
HIDDEN = bool
32+
RELEASE_DATE = Union[ReleaseDate, None]
33+
SOURCE = Union[Source, None]
34+
PLAYNITE = 'PlayniteApp'
35+
36+
@dataclass
37+
class Game:
38+
"""Playnite Game"""
39+
Id: ID
40+
Name: NAME
41+
Playtime: PLAYTIME
42+
IsInstalled: IS_INSTALLED
43+
InstallDirectory: INSTALL_DIRECTORY
44+
Icon: ICON
45+
Hidden: HIDDEN
46+
ReleaseDate: RELEASE_DATE
47+
Source: SOURCE
48+
Playnite: PLAYNITE = field(repr=False)
49+
50+
51+
def _build_uri(self, action, scheme=PLAYNITE_SCHEME) -> str:
52+
return f"{scheme}playnite/{action}/{self.Id}"
53+
54+
def get_release_date(self) -> datetime | None:
55+
if self.ReleaseDate:
56+
return datetime(self.ReleaseDate.Year, self.ReleaseDate.Month, self.ReleaseDate.Day)
57+
return None
58+
59+
@property
60+
def start_uri(self) -> str:
61+
return self._build_uri('start')
62+
63+
@property
64+
def show_game_uri(self) -> str:
65+
return self._build_uri('showgame')
66+
67+
@property
68+
def icon_path(self) -> Path | None:
69+
if self.Icon:
70+
path = Path(self.Playnite.path, 'library', 'files', self.Icon)
71+
if path.is_file():
72+
return path
73+
return None
74+
75+
def start(self) -> None:
76+
webbrowser.open(self.start_uri)
77+
78+
def show_game(self) -> None:
79+
webbrowser.open(self.show_game_uri)

plugin/main.py

Lines changed: 37 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,55 @@
11
import webbrowser
22
from pathlib import Path
3-
from difflib import SequenceMatcher as SM
43

5-
from flox import Flox, ICON_SETTINGS
6-
import playnite as pn
4+
from flox import Flox
5+
from playnite import DEFAULT_PLAYNITE_DIR, PlayniteApp
6+
from result import Result, OpenInPlaynite, LaunchGameContext
7+
from filters import IsInstalled, IsHidden
8+
from exceptions import PlayniteNotFound, LibraryNotFound
79

8-
9-
SOURCE_FILTER = '#'
10-
INSTALL_FILTER = '@'
11-
HIDDEN_FILTER = '!'
12-
SCORE_CUTOFF = 10
1310
PLUGIN_URI = 'playnite://playnite/installaddon/FlowLauncherExporter'
1411

15-
def match(query, text):
16-
return int(SM(
17-
lambda x: x == " ",
18-
query,
19-
text.lower()).ratio() * 100
20-
)
21-
2212
class Playnite(Flox):
2313

2414
def load_settings(self):
25-
self.playnite_path = self.settings.setdefault('playnite_path', str(pn.DATA_FOLDER))
15+
self.applied_filters = []
16+
self.playnite_path = self.settings.setdefault('playnite_path', str(DEFAULT_PLAYNITE_DIR))
2617
self.hide_uninstalled = self.settings.get('hide_uninstalled', True)
27-
28-
def missing_library(self):
29-
self.add_item(
30-
title='Library file not found!',
31-
subtitle="Please set the path to Playnite\'s data directory in settings.",
32-
icon=ICON_SETTINGS,
33-
method=self.open_setting_dialog
34-
)
35-
self.add_item(
36-
title='Install FlowLauncherExporter plugin.',
37-
subtitle='FlowLauncherExporter plugin is required to use this plugin.',
38-
icon='',
39-
method=self.uri,
40-
parameters=[PLUGIN_URI]
41-
42-
)
43-
44-
def main_search(self, query):
45-
for game in self.games:
46-
score = match(query, game.name)
47-
if score >= SCORE_CUTOFF or query == '':
48-
if game.is_installed:
49-
subtitle = game.install_directory
50-
uri = game.start_uri
51-
else:
52-
subtitle = 'Not Installed'
53-
uri = game.show_uri
54-
self.add_item(
55-
title=game.name,
56-
subtitle=f'{game.source["Name"]}: {subtitle}',
57-
icon=str(game.icon_path),
58-
method=self.uri,
59-
parameters=[uri],
60-
context=[game.show_uri],
61-
score=score
62-
)
63-
64-
def source_filter(self, query):
65-
sources = [game.source['Name'] for game in self.games]
66-
sources = set(sources)
67-
for source in sources:
68-
if source.lower() in query:
69-
query = query.replace(source.lower(), '').lstrip()
70-
self.games = [game for game in self.games if source.lower() == game.source['Name'].lower()]
71-
break
72-
if query in source.lower() or query == '':
73-
_ = self.add_item(
74-
title=f'{SOURCE_FILTER}{source}',
75-
subtitle='Filter by source.',
76-
icon='',
77-
method=self.change_query,
78-
dont_hide=True,
79-
)
80-
_['JsonRPCAction']['Parameters'] = [f"{_['AutoCompleteText']} "]
81-
else:
82-
self.games = []
83-
return query
84-
85-
def install_filter(self):
86-
self.games = [game for game in self.games if not game.is_installed]
87-
88-
def uninstalled_filter(self):
89-
self.games = [game for game in self.games if game.is_installed and (game.install_directory != None or game.install_directory != "")]
90-
91-
def remove_hidden(self):
92-
self.games = [game for game in self.games if not game.hidden]
18+
if self.hide_uninstalled:
19+
self.applied_filters.append(IsInstalled)
20+
# If has "Show Hidden" setting, hide hidden games
21+
if not self.settings.get('show_hidden', False):
22+
self.applied_filters.append(IsHidden(invert=True))
23+
self.pn = PlayniteApp(self.playnite_path)
9324

9425
def query(self, query):
95-
self.load_settings()
96-
query = query.lower()
9726
try:
98-
self.games = pn.import_games(self.playnite_path)
99-
except FileNotFoundError:
100-
self.missing_library()
101-
return
102-
if query.startswith(SOURCE_FILTER):
103-
query = query[len(SOURCE_FILTER):]
104-
query = self.source_filter(query)
105-
if INSTALL_FILTER in query:
106-
query = query.replace(INSTALL_FILTER, '')
107-
self.install_filter()
108-
elif self.hide_uninstalled:
109-
self.uninstalled_filter()
110-
if HIDDEN_FILTER not in query:
111-
self.remove_hidden()
112-
else:
113-
query = query.replace(HIDDEN_FILTER, '')
114-
self.main_search(query)
115-
27+
self.load_settings()
28+
games = self.pn.search(query, self.applied_filters)
29+
for game in games:
30+
self.add_item(
31+
**Result(game).to_dict()
32+
)
33+
except PlayniteNotFound:
34+
self.add_item(
35+
title='Playnite not found! Set the path in the settings.',
36+
subtitle='Open settings.',
37+
method=self.open_setting_dialog
38+
)
39+
except LibraryNotFound:
40+
self.add_item(
41+
title='Flow Launcher Exporter not found! Install it in Playnite.',
42+
subtitle='Click to install now.',
43+
method=self.uri,
44+
parameters=[PLUGIN_URI]
45+
)
11646

11747
def context_menu(self, data):
118-
show_uri = data[0]
119-
icon = str(self.icon if Path(self.icon).is_absolute() else Path(self.plugindir, self.icon))
120-
self.logger.warning(icon)
121-
self.add_item(
122-
title='Open in Playnite',
123-
subtitle='Shows Game in Playnite library.',
124-
icon=icon,
125-
method=self.uri,
126-
parameters=[show_uri],
127-
)
48+
self.load_settings()
49+
game = self.pn.game(data)
50+
if game:
51+
self.add_item(**OpenInPlaynite(game).to_dict())
52+
self.add_item(**LaunchGameContext(game).to_dict())
12853

12954
def uri(self, uri):
13055
webbrowser.open(uri)

0 commit comments

Comments
 (0)