Skip to content

Commit b744fdb

Browse files
authored
Merge pull request #1943 from oliv3r/profiled_search
Added: Profile-scoped search history in SearchAction
2 parents 3f40807 + e38b2ae commit b744fdb

File tree

4 files changed

+124
-7
lines changed

4 files changed

+124
-7
lines changed

resources/lib/actions/searchaction.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,25 @@ def __init__(self, parameter_parser: ActionParser, channel: Channel, needle: Opt
3838
self.__settings = AddonSettings.store(store_location=LOCAL)
3939
self.__media_item = parameter_parser.media_item
4040
self.__channel = channel
41+
self.__search_key = self.__get_search_key()
4142
Logger.debug(f"Searching for: {self.__needle}")
4243

44+
def __get_search_key(self) -> str:
45+
"""Return the settings key for search history.
46+
47+
When the channel provides a ``search_profile_id``, the key is scoped
48+
to that profile so each profile has its own search history.
49+
The active key is persisted so that Menu operations (clear, remove)
50+
can find it without instantiating the full channel.
51+
"""
52+
profile_id = self.__channel.search_profile_id
53+
if profile_id:
54+
key = f"search:{profile_id}"
55+
else:
56+
key = "search"
57+
self.__settings.set_setting("search:active_key", key, self.__channel)
58+
return key
59+
4360
def execute(self):
4461
# read the item from the parameters
4562
selected_item: MediaItem = self.__media_item
@@ -59,13 +76,13 @@ def execute(self):
5976
return
6077

6178
# noinspection PyTypeChecker
62-
history: List[str] = self.__settings.get_setting("search", self.__channel, []) # type: ignore
79+
history: List[str] = self.__settings.get_setting(self.__search_key, self.__channel, []) # type: ignore
6380
history = [needle] + history
6481
# de-duplicate without changing order:
6582
seen = set()
6683
history = [h for h in history if h not in seen and not seen.add(h)]
6784

68-
self.__settings.set_setting("search", history[0:10], self.__channel)
85+
self.__settings.set_setting(self.__search_key, history[0:10], self.__channel)
6986

7087
# Bug: empty needle is passed through, so a refresh triggers
7188
# the keyboard pop-up instead of re-running the query.
@@ -80,7 +97,7 @@ def execute(self):
8097

8198
def __generate_search_history(self, selected_item: MediaItem, parent_guid: str):
8299
# noinspection PyTypeChecker
83-
history: List[str] = self.__settings.get_setting("search", self.__channel, [])
100+
history: List[str] = self.__settings.get_setting(self.__search_key, self.__channel, [])
84101

85102
media_items = []
86103
search_item = FolderItem(

resources/lib/chn_class.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,18 @@ def init_channel(self):
140140
self.poster = TextureHandler.instance().get_texture_uri(self, self.poster)
141141
return
142142

143+
@property
144+
def search_profile_id(self) -> Optional[str]:
145+
"""Return the active profile ID for profile-scoped search history.
146+
147+
Channels that support user profiles should override this to return
148+
the active profile ID. When set, SearchAction stores search history
149+
per profile instead of per channel.
150+
151+
:return: The active profile ID, or None if profiles are not supported.
152+
"""
153+
return None
154+
143155
@property
144156
def search_url(self) -> str:
145157
if self.channelCode:

resources/lib/menu.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,22 +126,32 @@ def show_settings(self):
126126
AddonSettings.show_settings()
127127
self.refresh()
128128

129+
def __get_search_key(self) -> str:
130+
# SearchAction may store history under a profile-scoped key
131+
# (e.g. "search:{profile_id}"). We record the active key so
132+
# Menu can find it without instantiating the full channel.
133+
settings = AddonSettings.store(LOCAL)
134+
key = settings.get_setting("search:active_key", self.channelObject, None)
135+
return key or "search"
136+
129137
def clear_search(self):
130138
""" Clears the complete search history for a channel."""
131139

132140
settings = AddonSettings.store(LOCAL)
133-
settings.set_setting("search", [], self.channelObject)
141+
settings.set_setting(self.__get_search_key(), [], self.channelObject)
134142
self.refresh()
135143

136144
def remove_search_item(self):
137145
""" Removes a single item from the search history for a folder """
138146

139147
settings = AddonSettings.store(LOCAL)
140-
history: List[str] = settings.get_setting("search", self.channelObject, []) # type: ignore
148+
key = self.__get_search_key()
149+
history: List[str] = settings.get_setting(key, self.channelObject, []) # type: ignore
141150
needle: str = self.params[keyword.NEEDLE]
142151
needle = HtmlEntityHelper.url_decode(needle)
143-
history.remove(needle)
144-
settings.set_setting("search", history, self.channelObject)
152+
if needle in history:
153+
history.remove(needle)
154+
settings.set_setting(key, history, self.channelObject)
145155
self.refresh()
146156

147157
def channel_settings(self):

tests/test_search_history.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# SPDX-License-Identifier: GPL-3.0-or-later
2+
3+
import os
4+
import unittest
5+
6+
from resources.lib.logger import Logger
7+
from resources.lib.retroconfig import Config
8+
from resources.lib.settings.localsettings import LocalSettings
9+
10+
11+
class _FakeChannel:
12+
"""Minimal stand-in for a Channel with an ``id`` attribute."""
13+
14+
def __init__(self, channel_id):
15+
self.id = channel_id
16+
17+
18+
class TestSearchHistory(unittest.TestCase):
19+
"""Verify that profile-scoped search keys produce isolated histories."""
20+
21+
@classmethod
22+
def setUpClass(cls):
23+
Logger.create_logger(None, str(cls), min_log_level=0)
24+
25+
def setUp(self):
26+
local_settings_file = os.path.join(Config.profileDir, "settings.json")
27+
if os.path.isfile(local_settings_file):
28+
os.remove(local_settings_file)
29+
30+
def _store(self):
31+
return LocalSettings(Config.profileDir, Logger.instance())
32+
33+
def test_no_profile_uses_plain_key(self):
34+
"""Without a profile, the key is just ``search``."""
35+
channel = _FakeChannel("channel.nlziet.nlziet")
36+
store = self._store()
37+
38+
store.set_setting("search", ["cats"], channel)
39+
self.assertEqual(store.get_setting("search", channel, []), ["cats"])
40+
41+
def test_profile_scoped_key(self):
42+
"""With a profile, the key is ``search:<profile_id>``."""
43+
channel = _FakeChannel("channel.nlziet.nlziet")
44+
store = self._store()
45+
46+
key = "search:profile-abc"
47+
store.set_setting(key, ["dogs"], channel)
48+
self.assertEqual(store.get_setting(key, channel, []), ["dogs"])
49+
50+
def test_profiles_are_isolated(self):
51+
"""Different profile keys must not share history."""
52+
channel = _FakeChannel("channel.nlziet.nlziet")
53+
store = self._store()
54+
55+
store.set_setting("search:profile-adult", ["thriller"], channel)
56+
store.set_setting("search:profile-kids", ["cartoon"], channel)
57+
58+
self.assertEqual(
59+
store.get_setting("search:profile-adult", channel, []),
60+
["thriller"],
61+
)
62+
self.assertEqual(
63+
store.get_setting("search:profile-kids", channel, []),
64+
["cartoon"],
65+
)
66+
67+
def test_profile_key_does_not_affect_plain_key(self):
68+
"""Profile-scoped history and plain history are independent."""
69+
channel = _FakeChannel("channel.nlziet.nlziet")
70+
store = self._store()
71+
72+
store.set_setting("search", ["global"], channel)
73+
store.set_setting("search:profile-123", ["scoped"], channel)
74+
75+
self.assertEqual(store.get_setting("search", channel, []), ["global"])
76+
self.assertEqual(
77+
store.get_setting("search:profile-123", channel, []), ["scoped"]
78+
)

0 commit comments

Comments
 (0)