We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Support different seeds for random.hourly sort
These carousels are all sorted by random.hourly, but we want them to have a different random subset!

![image](https://github.com/internetarchive/openlibrary/assets/6251786/408dcd86-f96a-4534-bacf-46f437f9f444)


### Proposal & Constraints
Expand `random.hourly` sorting to support a custom seed like `random`

### Additional context
<!-- Add any other context or screenshots about the feature request here. -->

### Stakeholders
@RayBB 

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `openlibrary/plugins/worksearch/schemes/__init__.py`
Content:
```
1 import logging
2 from collections.abc import Callable
3 
4 import luqum.tree
5 from luqum.exceptions import ParseError
6 from openlibrary.solr.query_utils import (
7     escape_unknown_fields,
8     fully_escape_query,
9     luqum_parser,
10 )
11 
12 logger = logging.getLogger("openlibrary.worksearch")
13 
14 
15 class SearchScheme:
16     # Set of queries that define the universe of this scheme
17     universe: list[str]
18     # All actual solr fields that can be in a user query
19     all_fields: set[str]
20     # These fields are fetched for facets and can also be url params
21     facet_fields: set[str]
22     # Mapping of user-only fields to solr fields
23     field_name_map: dict[str, str]
24     # Mapping of user sort to solr sort
25     sorts: dict[str, str | Callable[[], str]]
26     # Default
27     default_fetched_fields: set[str]
28     # Fields that should be rewritten
29     facet_rewrites: dict[tuple[str, str], str | Callable[[], str]]
30 
31     def is_search_field(self, field: str):
32         return field in self.all_fields or field in self.field_name_map
33 
34     def process_user_sort(self, user_sort: str) -> str:
35         """
36         Convert a user-provided sort to a solr sort
37 
38         >>> from openlibrary.plugins.worksearch.schemes.works import WorkSearchScheme
39         >>> scheme = WorkSearchScheme()
40         >>> scheme.process_user_sort('editions')
41         'edition_count desc'
42         >>> scheme.process_user_sort('editions, new')
43         'edition_count desc,first_publish_year desc'
44         >>> scheme.process_user_sort('random')
45         'random_1 asc'
46         >>> scheme.process_user_sort('random_custom_seed')
47         'random_custom_seed asc'
48         >>> scheme.process_user_sort('random_custom_seed desc')
49         'random_custom_seed desc'
50         >>> scheme.process_user_sort('random_custom_seed asc')
51         'random_custom_seed asc'
52         """
53 
54         def process_individual_sort(sort: str):
55             if sort.startswith('random_'):
56                 # Allow custom randoms; so anything random_* is allowed
57                 return sort if ' ' in sort else f'{sort} asc'
58             else:
59                 solr_sort = self.sorts[sort]
60                 return solr_sort() if callable(solr_sort) else solr_sort
61 
62         return ','.join(
63             process_individual_sort(s.strip()) for s in user_sort.split(',')
64         )
65 
66     def process_user_query(self, q_param: str) -> str:
67         if q_param == '*:*':
68             # This is a special solr syntax; don't process
69             return q_param
70 
71         try:
72             q_param = escape_unknown_fields(
73                 (
74                     # Solr 4+ has support for regexes (eg `key:/foo.*/`)! But for now,
75                     # let's not expose that and escape all '/'. Otherwise
76                     # `key:/works/OL1W` is interpreted as a regex.
77                     q_param.strip()
78                     .replace('/', '\\/')
79                     # Also escape unexposed lucene features
80                     .replace('?', '\\?')
81                     .replace('~', '\\~')
82                 ),
83                 self.is_search_field,
84                 lower=True,
85             )
86             q_tree = luqum_parser(q_param)
87         except ParseError:
88             # This isn't a syntactically valid lucene query
89             logger.warning("Invalid lucene query", exc_info=True)
90             # Escape everything we can
91             q_tree = luqum_parser(fully_escape_query(q_param))
92 
93         q_tree = self.transform_user_query(q_param, q_tree)
94         return str(q_tree)
95 
96     def transform_user_query(
97         self,
98         user_query: str,
99         q_tree: luqum.tree.Item,
100     ) -> luqum.tree.Item:
101         return q_tree
102 
103     def build_q_from_params(self, params: dict) -> str | None:
104         return None
105 
106     def q_to_solr_params(
107         self,
108         q: str,
109         solr_fields: set[str],
110         cur_solr_params: list[tuple[str, str]],
111     ) -> list[tuple[str, str]]:
112         return [('q', q)]
113
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/openlibrary/plugins/worksearch/schemes/__init__.py b/openlibrary/plugins/worksearch/schemes/__init__.py
--- a/openlibrary/plugins/worksearch/schemes/__init__.py
+++ b/openlibrary/plugins/worksearch/schemes/__init__.py
@@ -44,17 +44,27 @@
         >>> scheme.process_user_sort('random')
         'random_1 asc'
         >>> scheme.process_user_sort('random_custom_seed')
-        'random_custom_seed asc'
+        'random_1_custom_seed asc'
         >>> scheme.process_user_sort('random_custom_seed desc')
-        'random_custom_seed desc'
+        'random_1_custom_seed desc'
         >>> scheme.process_user_sort('random_custom_seed asc')
-        'random_custom_seed asc'
+        'random_1_custom_seed asc'
         """
 
-        def process_individual_sort(sort: str):
-            if sort.startswith('random_'):
+        def process_individual_sort(sort: str) -> str:
+            if sort.startswith(('random_', 'random.hourly_', 'random.daily_')):
                 # Allow custom randoms; so anything random_* is allowed
-                return sort if ' ' in sort else f'{sort} asc'
+                # Also Allow custom time randoms to allow carousels with overlapping
+                # books to have a fresh ordering when on the same collection
+                sort_order: str | None = None
+                if ' ' in sort:
+                    sort, sort_order = sort.split(' ', 1)
+                random_type, random_seed = sort.split('_', 1)
+                solr_sort = self.sorts[random_type]
+                solr_sort_str = solr_sort() if callable(solr_sort) else solr_sort
+                solr_sort_field, solr_sort_order = solr_sort_str.split(' ', 1)
+                sort_order = sort_order or solr_sort_order
+                return f'{solr_sort_field}_{random_seed} {sort_order}'
             else:
                 solr_sort = self.sorts[sort]
                 return solr_sort() if callable(solr_sort) else solr_sort

{"golden_diff": "diff --git a/openlibrary/plugins/worksearch/schemes/__init__.py b/openlibrary/plugins/worksearch/schemes/__init__.py\n--- a/openlibrary/plugins/worksearch/schemes/__init__.py\n+++ b/openlibrary/plugins/worksearch/schemes/__init__.py\n@@ -44,17 +44,27 @@\n         >>> scheme.process_user_sort('random')\n         'random_1 asc'\n         >>> scheme.process_user_sort('random_custom_seed')\n-        'random_custom_seed asc'\n+        'random_1_custom_seed asc'\n         >>> scheme.process_user_sort('random_custom_seed desc')\n-        'random_custom_seed desc'\n+        'random_1_custom_seed desc'\n         >>> scheme.process_user_sort('random_custom_seed asc')\n-        'random_custom_seed asc'\n+        'random_1_custom_seed asc'\n         \"\"\"\n \n-        def process_individual_sort(sort: str):\n-            if sort.startswith('random_'):\n+        def process_individual_sort(sort: str) -> str:\n+            if sort.startswith(('random_', 'random.hourly_', 'random.daily_')):\n                 # Allow custom randoms; so anything random_* is allowed\n-                return sort if ' ' in sort else f'{sort} asc'\n+                # Also Allow custom time randoms to allow carousels with overlapping\n+                # books to have a fresh ordering when on the same collection\n+                sort_order: str | None = None\n+                if ' ' in sort:\n+                    sort, sort_order = sort.split(' ', 1)\n+                random_type, random_seed = sort.split('_', 1)\n+                solr_sort = self.sorts[random_type]\n+                solr_sort_str = solr_sort() if callable(solr_sort) else solr_sort\n+                solr_sort_field, solr_sort_order = solr_sort_str.split(' ', 1)\n+                sort_order = sort_order or solr_sort_order\n+                return f'{solr_sort_field}_{random_seed} {sort_order}'\n             else:\n                 solr_sort = self.sorts[sort]\n                 return solr_sort() if callable(solr_sort) else solr_sort\n", "issue": "Support different seeds for random.hourly sort\nThese carousels are all sorted by random.hourly, but we want them to have a different random subset!\r\n\r\n![image](https://github.com/internetarchive/openlibrary/assets/6251786/408dcd86-f96a-4534-bacf-46f437f9f444)\r\n\r\n\r\n### Proposal & Constraints\r\nExpand `random.hourly` sorting to support a custom seed like `random`\r\n\r\n### Additional context\r\n<!-- Add any other context or screenshots about the feature request here. -->\r\n\r\n### Stakeholders\r\n@RayBB \n", "before_files": [{"content": "import logging\nfrom collections.abc import Callable\n\nimport luqum.tree\nfrom luqum.exceptions import ParseError\nfrom openlibrary.solr.query_utils import (\n    escape_unknown_fields,\n    fully_escape_query,\n    luqum_parser,\n)\n\nlogger = logging.getLogger(\"openlibrary.worksearch\")\n\n\nclass SearchScheme:\n    # Set of queries that define the universe of this scheme\n    universe: list[str]\n    # All actual solr fields that can be in a user query\n    all_fields: set[str]\n    # These fields are fetched for facets and can also be url params\n    facet_fields: set[str]\n    # Mapping of user-only fields to solr fields\n    field_name_map: dict[str, str]\n    # Mapping of user sort to solr sort\n    sorts: dict[str, str | Callable[[], str]]\n    # Default\n    default_fetched_fields: set[str]\n    # Fields that should be rewritten\n    facet_rewrites: dict[tuple[str, str], str | Callable[[], str]]\n\n    def is_search_field(self, field: str):\n        return field in self.all_fields or field in self.field_name_map\n\n    def process_user_sort(self, user_sort: str) -> str:\n        \"\"\"\n        Convert a user-provided sort to a solr sort\n\n        >>> from openlibrary.plugins.worksearch.schemes.works import WorkSearchScheme\n        >>> scheme = WorkSearchScheme()\n        >>> scheme.process_user_sort('editions')\n        'edition_count desc'\n        >>> scheme.process_user_sort('editions, new')\n        'edition_count desc,first_publish_year desc'\n        >>> scheme.process_user_sort('random')\n        'random_1 asc'\n        >>> scheme.process_user_sort('random_custom_seed')\n        'random_custom_seed asc'\n        >>> scheme.process_user_sort('random_custom_seed desc')\n        'random_custom_seed desc'\n        >>> scheme.process_user_sort('random_custom_seed asc')\n        'random_custom_seed asc'\n        \"\"\"\n\n        def process_individual_sort(sort: str):\n            if sort.startswith('random_'):\n                # Allow custom randoms; so anything random_* is allowed\n                return sort if ' ' in sort else f'{sort} asc'\n            else:\n                solr_sort = self.sorts[sort]\n                return solr_sort() if callable(solr_sort) else solr_sort\n\n        return ','.join(\n            process_individual_sort(s.strip()) for s in user_sort.split(',')\n        )\n\n    def process_user_query(self, q_param: str) -> str:\n        if q_param == '*:*':\n            # This is a special solr syntax; don't process\n            return q_param\n\n        try:\n            q_param = escape_unknown_fields(\n                (\n                    # Solr 4+ has support for regexes (eg `key:/foo.*/`)! But for now,\n                    # let's not expose that and escape all '/'. Otherwise\n                    # `key:/works/OL1W` is interpreted as a regex.\n                    q_param.strip()\n                    .replace('/', '\\\\/')\n                    # Also escape unexposed lucene features\n                    .replace('?', '\\\\?')\n                    .replace('~', '\\\\~')\n                ),\n                self.is_search_field,\n                lower=True,\n            )\n            q_tree = luqum_parser(q_param)\n        except ParseError:\n            # This isn't a syntactically valid lucene query\n            logger.warning(\"Invalid lucene query\", exc_info=True)\n            # Escape everything we can\n            q_tree = luqum_parser(fully_escape_query(q_param))\n\n        q_tree = self.transform_user_query(q_param, q_tree)\n        return str(q_tree)\n\n    def transform_user_query(\n        self,\n        user_query: str,\n        q_tree: luqum.tree.Item,\n    ) -> luqum.tree.Item:\n        return q_tree\n\n    def build_q_from_params(self, params: dict) -> str | None:\n        return None\n\n    def q_to_solr_params(\n        self,\n        q: str,\n        solr_fields: set[str],\n        cur_solr_params: list[tuple[str, str]],\n    ) -> list[tuple[str, str]]:\n        return [('q', q)]\n", "path": "openlibrary/plugins/worksearch/schemes/__init__.py"}], "after_files": [{"content": "import logging\nfrom collections.abc import Callable\n\nimport luqum.tree\nfrom luqum.exceptions import ParseError\nfrom openlibrary.solr.query_utils import (\n    escape_unknown_fields,\n    fully_escape_query,\n    luqum_parser,\n)\n\nlogger = logging.getLogger(\"openlibrary.worksearch\")\n\n\nclass SearchScheme:\n    # Set of queries that define the universe of this scheme\n    universe: list[str]\n    # All actual solr fields that can be in a user query\n    all_fields: set[str]\n    # These fields are fetched for facets and can also be url params\n    facet_fields: set[str]\n    # Mapping of user-only fields to solr fields\n    field_name_map: dict[str, str]\n    # Mapping of user sort to solr sort\n    sorts: dict[str, str | Callable[[], str]]\n    # Default\n    default_fetched_fields: set[str]\n    # Fields that should be rewritten\n    facet_rewrites: dict[tuple[str, str], str | Callable[[], str]]\n\n    def is_search_field(self, field: str):\n        return field in self.all_fields or field in self.field_name_map\n\n    def process_user_sort(self, user_sort: str) -> str:\n        \"\"\"\n        Convert a user-provided sort to a solr sort\n\n        >>> from openlibrary.plugins.worksearch.schemes.works import WorkSearchScheme\n        >>> scheme = WorkSearchScheme()\n        >>> scheme.process_user_sort('editions')\n        'edition_count desc'\n        >>> scheme.process_user_sort('editions, new')\n        'edition_count desc,first_publish_year desc'\n        >>> scheme.process_user_sort('random')\n        'random_1 asc'\n        >>> scheme.process_user_sort('random_custom_seed')\n        'random_1_custom_seed asc'\n        >>> scheme.process_user_sort('random_custom_seed desc')\n        'random_1_custom_seed desc'\n        >>> scheme.process_user_sort('random_custom_seed asc')\n        'random_1_custom_seed asc'\n        \"\"\"\n\n        def process_individual_sort(sort: str) -> str:\n            if sort.startswith(('random_', 'random.hourly_', 'random.daily_')):\n                # Allow custom randoms; so anything random_* is allowed\n                # Also Allow custom time randoms to allow carousels with overlapping\n                # books to have a fresh ordering when on the same collection\n                sort_order: str | None = None\n                if ' ' in sort:\n                    sort, sort_order = sort.split(' ', 1)\n                random_type, random_seed = sort.split('_', 1)\n                solr_sort = self.sorts[random_type]\n                solr_sort_str = solr_sort() if callable(solr_sort) else solr_sort\n                solr_sort_field, solr_sort_order = solr_sort_str.split(' ', 1)\n                sort_order = sort_order or solr_sort_order\n                return f'{solr_sort_field}_{random_seed} {sort_order}'\n            else:\n                solr_sort = self.sorts[sort]\n                return solr_sort() if callable(solr_sort) else solr_sort\n\n        return ','.join(\n            process_individual_sort(s.strip()) for s in user_sort.split(',')\n        )\n\n    def process_user_query(self, q_param: str) -> str:\n        if q_param == '*:*':\n            # This is a special solr syntax; don't process\n            return q_param\n\n        try:\n            q_param = escape_unknown_fields(\n                (\n                    # Solr 4+ has support for regexes (eg `key:/foo.*/`)! But for now,\n                    # let's not expose that and escape all '/'. Otherwise\n                    # `key:/works/OL1W` is interpreted as a regex.\n                    q_param.strip()\n                    .replace('/', '\\\\/')\n                    # Also escape unexposed lucene features\n                    .replace('?', '\\\\?')\n                    .replace('~', '\\\\~')\n                ),\n                self.is_search_field,\n                lower=True,\n            )\n            q_tree = luqum_parser(q_param)\n        except ParseError:\n            # This isn't a syntactically valid lucene query\n            logger.warning(\"Invalid lucene query\", exc_info=True)\n            # Escape everything we can\n            q_tree = luqum_parser(fully_escape_query(q_param))\n\n        q_tree = self.transform_user_query(q_param, q_tree)\n        return str(q_tree)\n\n    def transform_user_query(\n        self,\n        user_query: str,\n        q_tree: luqum.tree.Item,\n    ) -> luqum.tree.Item:\n        return q_tree\n\n    def build_q_from_params(self, params: dict) -> str | None:\n        return None\n\n    def q_to_solr_params(\n        self,\n        q: str,\n        solr_fields: set[str],\n        cur_solr_params: list[tuple[str, str]],\n    ) -> list[tuple[str, str]]:\n        return [('q', q)]\n", "path": "openlibrary/plugins/worksearch/schemes/__init__.py"}]}