Coverage for src/lanraragi_api/api/search.py: 55%

22 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-22 23:19 +0000

1from typing import Any, cast 

2 

3from lanraragi_api.api.base import ( 

4 BaseAPICall, 

5) 

6from lanraragi_api.entity.archive import ArchiveMetadata 

7from lanraragi_api.entity.base import OperationResponse 

8from lanraragi_api.entity.search import SearchIdsResult, SearchResult 

9from lanraragi_api.error import APIResponseDecodeError 

10 

11 

12class SearchAPI(BaseAPICall): 

13 """Perform searches. 

14 

15 Shared request and error behavior is documented on ``BaseAPICall``. 

16 """ 

17 

18 def search_archives( 

19 self, 

20 category: str | None = None, 

21 filter: str | None = None, 

22 start: int | None = None, 

23 sortby: str | None = None, 

24 order: str | None = None, 

25 newonly: bool | None = None, 

26 untaggedonly: bool | None = None, 

27 hidecompleted: bool | None = None, 

28 groupby_tanks: bool | None = None, 

29 ) -> SearchResult: 

30 """Search for Archives. 

31 

32 You can use the IDs of this JSON with the other endpoints. 

33 

34 The ``filter`` parameter accepts the following special characters: 

35 

36 - Quotation marks (``"..."``): exact string search. Allows a search 

37 term to include spaces, as everything inside a pair of quotation 

38 marks is treated as a singular term. Wildcard characters are still 

39 interpreted as wildcards. 

40 - Question mark (``?``), underscore (``_``): wildcard matching any 

41 single character. 

42 - Asterisk (``*``), percentage sign (``%``): wildcard matching any 

43 sequence of characters, including none. 

44 - Subtraction sign (``-``): exclusion. When placed before a term, it 

45 prevents search results from including that term. 

46 - Dollar sign (``$``): add at the end of a tag to perform an exact tag 

47 search rather than displaying all elements that start with the term. 

48 Only matches tags regardless of the search parameters, and can be 

49 used as an exclusion to ignore misc tags in the search query. 

50 

51 Args: 

52 category: ID of the category you want to restrict this search to. 

53 Defaults to None. 

54 filter: Search query, using the special characters listed above. 

55 Defaults to None. 

56 start: From which archive in the total result count this 

57 enumeration should start. The total number of archives 

58 displayed depends on the server-side page size preference. 

59 From 0.8.2 onwards, "-1" gives the full, unpaged data. Defaults 

60 to None. 

61 sortby: Namespace by which you want to sort the results. Use 

62 ``title`` to sort by title, or ``lastread`` to sort by last 

63 read time, which requires Server-side Progress Tracking to be 

64 enabled. Defaults to None, which sorts by title; sorting by 

65 ``lastread`` removes IDs that have never been read from the 

66 search. 

67 order: Order of the sort, either ``asc`` or ``desc``. Defaults to 

68 None, which the server treats as ``asc``. 

69 newonly: Limit search to new archives only. Defaults to None. 

70 untaggedonly: Limit search to untagged archives only. Defaults to 

71 None. 

72 hidecompleted: Hide archives where reading progress has reached the 

73 end. Defaults to None. 

74 groupby_tanks: Enable or disable Tankoubon grouping. When enabled, 

75 Tankoubons show in search results, replacing all the archive 

76 IDs they contain. Defaults to None, which the server treats as 

77 true. 

78 

79 Returns: 

80 SearchResult: One ``ArchiveMetadata`` object per matching archive 

81 in ``data``. 

82 

83 Raises: 

84 APIHttpError: Any status code other than 200. 

85 APIResponseDecodeError: If the response body is not valid JSON, or 

86 does not match ``SearchResult``. 

87 

88 Note: 

89 The server answers with ``204`` when the search engine is not 

90 initialized yet. That response carries no body and therefore raises 

91 ``APIResponseDecodeError``; wait a few seconds and retry the search 

92 in that case. 

93 """ 

94 

95 return self.request_model( 

96 "GET", 

97 "/api/search", 

98 SearchResult, 

99 params={ 

100 "category": category, 

101 "filter": filter, 

102 "start": start, 

103 "sortby": sortby, 

104 "order": order, 

105 "newonly": newonly, 

106 "untaggedonly": untaggedonly, 

107 "hidecompleted": hidecompleted, 

108 "groupby_tanks": groupby_tanks, 

109 }, 

110 ) 

111 

112 def search( 

113 self, 

114 category: str | None = None, 

115 filter: str | None = None, 

116 start: int | None = None, 

117 sort_by: str = "title", 

118 order: str = "asc", 

119 new_only: bool = False, 

120 untagged_only: bool = False, 

121 hide_completed: bool | None = None, 

122 groupby_tanks: bool = True, 

123 ) -> SearchResult: 

124 """Search for Archives using the legacy parameter names. 

125 

126 This compatibility wrapper forwards every argument to 

127 ``search_archives``, renaming ``sort_by`` to ``sortby``, ``new_only`` 

128 to ``newonly``, ``untagged_only`` to ``untaggedonly`` and 

129 ``hide_completed`` to ``hidecompleted``. ``groupby_tanks`` keeps its 

130 name. 

131 

132 Args: 

133 category: ID of the category you want to restrict this search to. 

134 Defaults to None. 

135 filter: Search query, following the rules of ``search_archives``. 

136 Defaults to None. 

137 start: From which archive in the total result count this 

138 enumeration should start, with "-1" for the full, unpaged 

139 data. Defaults to None. 

140 sort_by: Namespace by which you want to sort the results, sent as 

141 ``sortby``. Defaults to "title". 

142 order: Order of the sort, either ``asc`` or ``desc``, sent as 

143 ``order``. Defaults to "asc". 

144 new_only: Limit search to new archives only, sent as ``newonly``. 

145 Defaults to False. 

146 untagged_only: Limit search to untagged archives only, sent as 

147 ``untaggedonly``. Defaults to False. 

148 hide_completed: Hide archives where reading progress has reached 

149 the end, sent as ``hidecompleted``. Defaults to None. 

150 groupby_tanks: Enable or disable Tankoubon grouping. Defaults to 

151 True. 

152 

153 Returns: 

154 SearchResult: Same result as ``search_archives``. 

155 

156 Raises: 

157 APIHttpError: Any status code other than 200. 

158 APIResponseDecodeError: If the response body is not valid JSON, or 

159 does not match ``SearchResult``. 

160 

161 Note: 

162 Falsy values are not forwarded to the server: ``new_only``, 

163 ``untagged_only``, ``hide_completed`` and ``groupby_tanks`` become 

164 ``None``, so the server-side default applies instead. 

165 """ 

166 return self.search_archives( 

167 category=category, 

168 filter=filter, 

169 start=start, 

170 sortby=sort_by, 

171 order=order, 

172 newonly=new_only if new_only else None, 

173 untaggedonly=untagged_only if untagged_only else None, 

174 hidecompleted=hide_completed if hide_completed else None, 

175 groupby_tanks=groupby_tanks if groupby_tanks else None, 

176 ) 

177 

178 def search_archive_ids( 

179 self, 

180 category: str | None = None, 

181 filter: str | None = None, 

182 start: int | None = None, 

183 sortby: str | None = None, 

184 order: str | None = None, 

185 newonly: bool | None = None, 

186 untaggedonly: bool | None = None, 

187 hidecompleted: bool | None = None, 

188 groupby_tanks: bool | None = None, 

189 ) -> SearchIdsResult: 

190 """Search for Archives like ``/api/search``, but return only IDs. 

191 

192 The ordered list of matching Archive IDs is returned without the 

193 accompanying metadata. 

194 

195 Args: 

196 category: ID of the category you want to restrict this search to. 

197 Defaults to None. 

198 filter: Search query, following the same rules as the queries in 

199 ``/api/search``. Defaults to None. 

200 start: From which archive in the total result count this 

201 enumeration should start. The total number of archives 

202 displayed depends on the server-side page size preference. 

203 From 0.8.2 onwards, "-1" gives the full, unpaged list of IDs. 

204 Defaults to None. 

205 sortby: Namespace by which you want to sort the results. Use 

206 ``title`` to sort by title, or ``lastread`` to sort by last 

207 read time, which requires Server-side Progress Tracking to be 

208 enabled. Defaults to None, which sorts by title; sorting by 

209 ``lastread`` removes IDs that have never been read from the 

210 search. 

211 order: Order of the sort, either ``asc`` or ``desc``. Defaults to 

212 None, which the server treats as ``asc``. 

213 newonly: Limit search to new archives only. Defaults to None. 

214 untaggedonly: Limit search to untagged archives only. Defaults to 

215 None. 

216 hidecompleted: Hide archives where reading progress has reached the 

217 end. Defaults to None. 

218 groupby_tanks: Enable or disable Tankoubon grouping. When enabled, 

219 Tankoubons show in search results, replacing all the archive 

220 IDs they contain. Defaults to None, which the server treats as 

221 true. 

222 

223 Returns: 

224 SearchIdsResult: One archive ID per matching archive in ``data``. 

225 

226 Raises: 

227 APIHttpError: Any status code other than 200. 

228 APIResponseDecodeError: If the response body is not valid JSON, or 

229 does not match ``SearchIdsResult``. 

230 

231 Note: 

232 The server answers with ``204`` when the search engine is not 

233 initialized yet. That response carries no body and therefore raises 

234 ``APIResponseDecodeError``; wait a few seconds and retry the search 

235 in that case. 

236 """ 

237 return self.request_model( 

238 "GET", 

239 "/api/search/ids", 

240 SearchIdsResult, 

241 params={ 

242 "category": category, 

243 "filter": filter, 

244 "start": start, 

245 "sortby": sortby, 

246 "order": order, 

247 "newonly": newonly, 

248 "untaggedonly": untaggedonly, 

249 "hidecompleted": hidecompleted, 

250 "groupby_tanks": groupby_tanks, 

251 }, 

252 ) 

253 

254 def get_random_archives( 

255 self, 

256 category: str | None = None, 

257 filter: str | None = None, 

258 count: int = 5, 

259 new_only: bool = False, 

260 untagged_only: bool = False, 

261 hide_completed: bool | None = None, 

262 groupby_tanks: bool = True, 

263 ) -> list[ArchiveMetadata]: 

264 """Get randomly selected Archives from the given filter and/or category. 

265 

266 Args: 

267 category: ID of the category you want to restrict this search to. 

268 Defaults to None. 

269 filter: Search query, following the same rules as the queries in 

270 ``/api/search``. Defaults to None. 

271 count: How many archives you want to pull randomly. If the search 

272 doesn't return enough data to match your count, you will get 

273 the full search shuffled randomly. Defaults to 5. 

274 new_only: Limit search to new archives only. Defaults to False, 

275 which is not sent to the server. 

276 untagged_only: Limit search to untagged archives only. Defaults to 

277 False, which is not sent to the server. 

278 hide_completed: Hide archives where reading progress has reached 

279 the end. Defaults to None. 

280 groupby_tanks: Enable or disable Tankoubon grouping. When enabled, 

281 Tankoubons show in search results, replacing all the archive 

282 IDs they contain. Defaults to True; a falsy value is not sent 

283 to the server. 

284 

285 Returns: 

286 list[ArchiveMetadata]: Randomly selected archives, one object per 

287 archive. 

288 

289 Raises: 

290 APIHttpError: Any status code other than 200. 

291 APIResponseDecodeError: If the response body is not valid JSON, if 

292 it has no ``data`` list, or if an item of that list does not 

293 match ``ArchiveMetadata``. 

294 """ 

295 

296 path = "/api/search/random" 

297 payload = self.request_json( 

298 "GET", 

299 path, 

300 params={ 

301 "category": category, 

302 "filter": filter, 

303 "count": count, 

304 "newonly": new_only if new_only else None, 

305 "untaggedonly": untagged_only if untagged_only else None, 

306 "hidecompleted": hide_completed if hide_completed else None, 

307 "groupby_tanks": groupby_tanks if groupby_tanks else None, 

308 }, 

309 ) 

310 data = payload.get("data") 

311 if not isinstance(data, list): 

312 raise APIResponseDecodeError(self._to_url(path), "missing data list") 

313 return [ 

314 self.parse_model(ArchiveMetadata, a, path) for a in cast(list[Any], data) 

315 ] 

316 

317 def discard_search_cache(self) -> OperationResponse: 

318 """Discard the cache containing previous user searches. 

319 

320 Returns: 

321 OperationResponse: Result of the cache discard operation. 

322 

323 Raises: 

324 APIResponseDecodeError: If the response body is not valid JSON, or 

325 does not match ``OperationResponse``. 

326 APIOperationError: If the operation failed and raising is enabled. 

327 

328 Note: 

329 A 400 response is returned in the operation result, with ``success`` 

330 set to 0, instead of raising. 

331 """ 

332 return self.request_operation("DELETE", "/api/search/cache")