@omelhorsite/sdk 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +4939 -552
- package/dist/types/client.d.ts +60 -3
- package/dist/types/http.d.ts +444 -19
- package/dist/types/index.d.ts +4 -1
- package/dist/types/resources/account.d.ts +66 -3
- package/dist/types/resources/admin.d.ts +1837 -0
- package/dist/types/resources/auth/index.d.ts +39 -0
- package/dist/types/resources/auth/passkeys.d.ts +652 -0
- package/dist/types/resources/auth/sessions.d.ts +847 -0
- package/dist/types/resources/chests.d.ts +54 -3
- package/dist/types/resources/content.d.ts +2970 -0
- package/dist/types/resources/dynamicQrs.d.ts +39 -3
- package/dist/types/resources/forms.d.ts +176 -35
- package/dist/types/resources/index.d.ts +19 -8
- package/dist/types/resources/ipLookup.d.ts +20 -4
- package/dist/types/resources/jobs.d.ts +62 -21
- package/dist/types/resources/library.d.ts +1435 -0
- package/dist/types/resources/linkTrees.d.ts +142 -30
- package/dist/types/resources/media.d.ts +351 -0
- package/dist/types/resources/movies.d.ts +1186 -0
- package/dist/types/resources/music/artists.d.ts +1066 -0
- package/dist/types/resources/music/imports.d.ts +940 -0
- package/dist/types/resources/music/index.d.ts +61 -0
- package/dist/types/resources/music/playlists.d.ts +1026 -0
- package/dist/types/resources/music/social.d.ts +1132 -0
- package/dist/types/resources/music/songs.d.ts +1183 -0
- package/dist/types/resources/notepads.d.ts +4 -1
- package/dist/types/resources/quotas.d.ts +7 -1
- package/dist/types/resources/realtime.d.ts +855 -0
- package/dist/types/resources/shortLinks.d.ts +45 -4
- package/dist/types/resources/social.d.ts +1330 -0
- package/dist/types/resources/storage/upload.d.ts +158 -11
- package/dist/types/resources/storage.d.ts +88 -22
- package/dist/types/resources/tickets.d.ts +82 -3
- package/dist/types/resources/tools/backgroundRemoval.d.ts +18 -3
- package/dist/types/resources/tools/captions.d.ts +448 -21
- package/dist/types/resources/tools/downloader.d.ts +21 -0
- package/dist/types/resources/tools/index.d.ts +57 -15
- package/dist/types/resources/tools/jumpstyle.d.ts +50 -17
- package/dist/types/resources/tools/transcription.d.ts +35 -13
- package/dist/types/resources/tools/upscale.d.ts +23 -3
- package/dist/types/resources/tools/vocalSeparation.d.ts +30 -13
- package/dist/types/types.d.ts +249 -17
- package/package.json +2 -1
|
@@ -0,0 +1,1186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `movies` namespace: the Stremio-style movie and series app.
|
|
3
|
+
*
|
|
4
|
+
* Six endpoint families live here because they are one app's worth of API:
|
|
5
|
+
* the addons that supply catalogues and streams, the groups and grants that
|
|
6
|
+
* share those addons with friends, the collections a user files titles into,
|
|
7
|
+
* and the watch progress that drives "Continuar a ver". They are exposed as
|
|
8
|
+
* one entry class ({@link MoviesNamespace}) with sub-namespaces hanging off it
|
|
9
|
+
* ({@link MoviesNamespace.addons}, `.addons.groups`, `.addons.grants`,
|
|
10
|
+
* `.collections`, `.collections.items`, `.watchProgress`), and every
|
|
11
|
+
* sub-namespace is also exported on its own so a host that prefers
|
|
12
|
+
* `oms.movieCollections` can mount it there instead.
|
|
13
|
+
*
|
|
14
|
+
* ## Seven things that have already cost bugs
|
|
15
|
+
*
|
|
16
|
+
* 1. **Every id here is an opaque 12-character STRING**, minted by
|
|
17
|
+
* `RandomIdentifier`, not an auto-increment integer like the music tables.
|
|
18
|
+
* They do not sort by creation and there is no arithmetic to do on one.
|
|
19
|
+
* 2. **The whole namespace requires a session.** `ApplicationController`
|
|
20
|
+
* requires authentication by default and none of the six controllers opts
|
|
21
|
+
* out, so an anonymous call is `401 "Session required to access this
|
|
22
|
+
* resource."` - not an empty list. Worse for OAuth: no movies controller
|
|
23
|
+
* declares an `oauth_scope`, and the scope gate denies by omission, so an
|
|
24
|
+
* OAuth access token gets `403 {"error":"insufficient_scope"}` on every
|
|
25
|
+
* route in this file. Cookie sessions and personal tokens work; third-party
|
|
26
|
+
* OAuth clients do not. See {@link MoviesNamespace}.
|
|
27
|
+
* 3. **`PATCH /movie_addons/:id` wipes `manifest_json` unless you resend it.**
|
|
28
|
+
* The controller assigns the key unconditionally, so a partial patch sends
|
|
29
|
+
* `nil` into a `presence: true` validation and answers `400`. That is why
|
|
30
|
+
* {@link UpdateMovieAddonInput.manifest_json} is REQUIRED here, and why
|
|
31
|
+
* {@link MovieAddonsNamespace.moveToGroup} exists.
|
|
32
|
+
* 4. **Listing collections has a side effect**: it is the only place that
|
|
33
|
+
* creates the user's "Favoritos" row. See
|
|
34
|
+
* {@link MovieCollectionsNamespace.list}.
|
|
35
|
+
* 5. **`POST /movie_watch_progresses` is an upsert that answers `200`, not
|
|
36
|
+
* `201`**, and its identity is `(user, movie_id, video_id)` - `movie_type`
|
|
37
|
+
* is NOT part of the key. It is one of the very few creates in the API that
|
|
38
|
+
* breaks the 201 convention, because it is not really a create.
|
|
39
|
+
* 6. **`finished` is a three-state field, not a boolean.** Omitted means "you
|
|
40
|
+
* decide from the position"; `true`/`false` means "the user said so". A
|
|
41
|
+
* `null` is deleted by the controller and reads as omitted, which is
|
|
42
|
+
* exactly the bug that once made "marcar como nao visto" silently do
|
|
43
|
+
* nothing. See {@link MovieWatchProgressInput.finished}.
|
|
44
|
+
* 7. **`last_watched_at` is only defaulted on INSERT.** The model does
|
|
45
|
+
* `self.last_watched_at ||= Time.current`, so an upsert onto an existing row
|
|
46
|
+
* that omits it keeps the OLD timestamp - and the Continue Watching list is
|
|
47
|
+
* ordered by exactly that column. Always send one. See
|
|
48
|
+
* {@link MovieWatchProgressInput.last_watched_at}.
|
|
49
|
+
*
|
|
50
|
+
* Everything here rides the general ceiling: **600 requests per minute** for an
|
|
51
|
+
* authenticated caller. There is no movie-specific rack-attack bucket, and
|
|
52
|
+
* since anonymous callers cannot reach any of it, the 120/min anonymous bucket
|
|
53
|
+
* never applies.
|
|
54
|
+
*/
|
|
55
|
+
import { ApiClient, Resource } from "../http";
|
|
56
|
+
import type { BaseRecord, Id, PageParams, Paginated, RequestOptions, Timestamp } from "../types";
|
|
57
|
+
import type { User } from "./account";
|
|
58
|
+
/**
|
|
59
|
+
* What a title is. Stremio's own vocabulary, and the backend stores it as a
|
|
60
|
+
* free string with no inclusion validation, so an addon may invent one.
|
|
61
|
+
* Compare against this union for the cases you handle and fall through for the
|
|
62
|
+
* rest rather than assuming the list is closed.
|
|
63
|
+
*/
|
|
64
|
+
export type MovieType = "movie" | "series" | "channel" | "tv" | (string & {});
|
|
65
|
+
/** The four resource names a Stremio manifest may advertise. */
|
|
66
|
+
export type StremioResourceName = "catalog" | "meta" | "stream" | "subtitles";
|
|
67
|
+
/** One catalogue an addon offers, as declared in its manifest. */
|
|
68
|
+
export interface StremioCatalog {
|
|
69
|
+
readonly type: string;
|
|
70
|
+
readonly id: string;
|
|
71
|
+
readonly name?: string;
|
|
72
|
+
readonly extra?: ReadonlyArray<{
|
|
73
|
+
readonly name: string;
|
|
74
|
+
readonly isRequired?: boolean;
|
|
75
|
+
readonly options?: readonly string[];
|
|
76
|
+
}>;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* An addon's `manifest.json`, stored verbatim in a `jsonb` column.
|
|
80
|
+
*
|
|
81
|
+
* The backend does not validate a single key of it beyond "not blank": it is
|
|
82
|
+
* `params[:manifest_json].to_unsafe_h`, written straight to the column and read
|
|
83
|
+
* straight back. So the fields below are what a well-behaved Stremio addon
|
|
84
|
+
* sends, not a contract the server enforces - `id` and `name` can be missing
|
|
85
|
+
* on a hostile or broken manifest even though they are typed as required here,
|
|
86
|
+
* and the index signature is there because whatever else the addon declared
|
|
87
|
+
* round-trips untouched.
|
|
88
|
+
*
|
|
89
|
+
* Never trust `logo`, `background` or any URL inside one without checking the
|
|
90
|
+
* origin: this blob is user-supplied content that the app renders.
|
|
91
|
+
*/
|
|
92
|
+
export interface StremioManifest {
|
|
93
|
+
readonly id: string;
|
|
94
|
+
readonly name: string;
|
|
95
|
+
readonly description?: string;
|
|
96
|
+
readonly version?: string;
|
|
97
|
+
readonly resources?: ReadonlyArray<StremioResourceName | {
|
|
98
|
+
readonly name: StremioResourceName;
|
|
99
|
+
readonly types?: readonly string[];
|
|
100
|
+
}>;
|
|
101
|
+
readonly types?: readonly string[];
|
|
102
|
+
readonly catalogs?: readonly StremioCatalog[];
|
|
103
|
+
readonly logo?: string;
|
|
104
|
+
readonly background?: string;
|
|
105
|
+
/** Anything else the manifest carried. `jsonb` keeps it all. */
|
|
106
|
+
readonly [key: string]: unknown;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* An installed addon: a manifest URL plus the manifest fetched from it.
|
|
110
|
+
*
|
|
111
|
+
* Rows reach a caller two ways, and {@link MovieAddon.shared} is how you tell
|
|
112
|
+
* them apart: the ones the caller installed, and the ones somebody granted
|
|
113
|
+
* them (directly, or through a group). A shared addon is read-only in
|
|
114
|
+
* practice - `updatable_by?` and `destroyable_by?` both require ownership, so
|
|
115
|
+
* every write against one is a `401`.
|
|
116
|
+
*/
|
|
117
|
+
export interface MovieAddon extends BaseRecord {
|
|
118
|
+
/** Owner. Compare against your own id, or just read {@link shared}. */
|
|
119
|
+
readonly user_id: Id;
|
|
120
|
+
/** Group the owner filed it under, or `null` when it is ungrouped. */
|
|
121
|
+
readonly movie_addon_group_id: Id | null;
|
|
122
|
+
/** Where the manifest was fetched from. Unique per owner. */
|
|
123
|
+
readonly manifest_url: string;
|
|
124
|
+
/** The manifest itself, stored verbatim. */
|
|
125
|
+
readonly manifest_json: StremioManifest;
|
|
126
|
+
/**
|
|
127
|
+
* `true` when this row belongs to somebody else and reached you through a
|
|
128
|
+
* grant. Computed against the CALLER, so the same row is `false` for its
|
|
129
|
+
* owner and `true` for everyone it is shared with - never cache it across
|
|
130
|
+
* identities.
|
|
131
|
+
*/
|
|
132
|
+
readonly shared: boolean;
|
|
133
|
+
}
|
|
134
|
+
/** Arguments for {@link MovieAddonsNamespace.create}. */
|
|
135
|
+
export interface CreateMovieAddonInput {
|
|
136
|
+
/**
|
|
137
|
+
* Absolute `http`/`https` URL of the manifest. Validated with
|
|
138
|
+
* `URI::DEFAULT_PARSER.make_regexp`, so anything else is
|
|
139
|
+
* `400 "Manifest url must be a valid URL"`.
|
|
140
|
+
*/
|
|
141
|
+
readonly manifest_url: string;
|
|
142
|
+
/**
|
|
143
|
+
* The fetched manifest. Required: `presence: true` rejects both `nil` and
|
|
144
|
+
* `{}`, so an addon whose manifest failed to download cannot be stored as a
|
|
145
|
+
* placeholder.
|
|
146
|
+
*/
|
|
147
|
+
readonly manifest_json: StremioManifest;
|
|
148
|
+
/**
|
|
149
|
+
* Group to file it under. The group must belong to the SAME user, or the
|
|
150
|
+
* save fails with `400 "Movie addon group must belong to addon owner"`.
|
|
151
|
+
*/
|
|
152
|
+
readonly movie_addon_group_id?: Id | null;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Arguments for {@link MovieAddonsNamespace.update}.
|
|
156
|
+
*
|
|
157
|
+
* `manifest_json` is required, and that is not an oversight to work around.
|
|
158
|
+
* `MovieAddonsController#movie_addon_params` is shared by create and update and
|
|
159
|
+
* ends with an unconditional `permitted[:manifest_json] = params[:manifest_json]`,
|
|
160
|
+
* so a PATCH that omits the key assigns `nil` over the stored manifest and then
|
|
161
|
+
* trips `validates :manifest_json, presence: true`. The row survives (the save
|
|
162
|
+
* failed) but the call is a `400`, which is how `moveAddonToGroup` in the web
|
|
163
|
+
* frontend is broken today - it sends `{ movie_addon_group_id }` alone. Resend
|
|
164
|
+
* the manifest you already hold; {@link MovieAddonsNamespace.moveToGroup} does
|
|
165
|
+
* it for you.
|
|
166
|
+
*/
|
|
167
|
+
export interface UpdateMovieAddonInput {
|
|
168
|
+
/** Resend the stored manifest verbatim. See the interface docs for why. */
|
|
169
|
+
readonly manifest_json: StremioManifest;
|
|
170
|
+
/** `null` un-groups the addon. Omitting the key leaves the group alone. */
|
|
171
|
+
readonly movie_addon_group_id?: Id | null;
|
|
172
|
+
/**
|
|
173
|
+
* Only if you really are re-pointing the addon. Changing it can collide with
|
|
174
|
+
* the `(user_id, manifest_url)` unique index, which surfaces as
|
|
175
|
+
* `400 "Manifest url has already been taken"`.
|
|
176
|
+
*/
|
|
177
|
+
readonly manifest_url?: string;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Filters for {@link MovieAddonsNamespace.list}.
|
|
181
|
+
*
|
|
182
|
+
* The server's allowlist for this index is exactly `user_id`, `id`,
|
|
183
|
+
* `created_at` and `updated_at`. Any other filter key is
|
|
184
|
+
* `400 "Unknown search filter: ..."` - filters fail closed rather than widening
|
|
185
|
+
* the query - so there is deliberately no way to list by
|
|
186
|
+
* `movie_addon_group_id`. Group client-side off the field on each row.
|
|
187
|
+
*/
|
|
188
|
+
export interface ListMovieAddonsParams extends PageParams {
|
|
189
|
+
/** Exact match. Use it to split your own addons from the shared ones. */
|
|
190
|
+
readonly userId?: Id;
|
|
191
|
+
/** Exact match on the primary key. */
|
|
192
|
+
readonly id?: Id;
|
|
193
|
+
}
|
|
194
|
+
/** Arguments for {@link MovieAddonGroupsNamespace.create}. */
|
|
195
|
+
export interface CreateMovieAddonGroupInput {
|
|
196
|
+
/** Required, at most {@link MOVIE_ADDON_GROUP_NAME_MAX_LENGTH} characters. */
|
|
197
|
+
readonly name: string;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* A named folder for addons, owned by one user.
|
|
201
|
+
*
|
|
202
|
+
* A group is also a sharing unit: granting a group shares every addon inside
|
|
203
|
+
* it, including ones added later, which is the whole reason groups exist.
|
|
204
|
+
*/
|
|
205
|
+
export interface MovieAddonGroup extends BaseRecord {
|
|
206
|
+
readonly user_id: Id;
|
|
207
|
+
readonly name: string;
|
|
208
|
+
}
|
|
209
|
+
/** Filters for {@link MovieAddonGroupsNamespace.list}. */
|
|
210
|
+
export interface ListMovieAddonGroupsParams extends PageParams {
|
|
211
|
+
/** Exact match on the primary key. The only filter this index allows. */
|
|
212
|
+
readonly id?: Id;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* A share: one addon, or one whole group, handed to one other user.
|
|
216
|
+
*
|
|
217
|
+
* Exactly one of `movie_addon_id` and `movie_addon_group_id` is set; the other
|
|
218
|
+
* is `null`. A database check constraint enforces it as well as the model, so
|
|
219
|
+
* there is no path to a row with both or neither.
|
|
220
|
+
*/
|
|
221
|
+
export interface MovieAddonGrant extends BaseRecord {
|
|
222
|
+
/** Set when this grant targets a single addon. */
|
|
223
|
+
readonly movie_addon_id: Id | null;
|
|
224
|
+
/** Set when this grant targets a whole group. */
|
|
225
|
+
readonly movie_addon_group_id: Id | null;
|
|
226
|
+
/** Who shared. Always the caller for a grant you created. */
|
|
227
|
+
readonly grantor_id: Id;
|
|
228
|
+
/** Who received. */
|
|
229
|
+
readonly grantee_id: Id;
|
|
230
|
+
/**
|
|
231
|
+
* The grantee, rendered as a full user.
|
|
232
|
+
*
|
|
233
|
+
* The web frontend types this as `{ id, name, handle }`; that is a subset,
|
|
234
|
+
* not the payload. `MovieAddonGrantBlueprint` does
|
|
235
|
+
* `JSON.parse(grant.grantee.render)`, which is `UserBlueprint`'s DEFAULT view
|
|
236
|
+
* with `Current.user` injected as the viewer - so `bio`, `country_code`, the
|
|
237
|
+
* `library_*` fields and the visibility flags all come along, and `email` /
|
|
238
|
+
* `gender` / `group` appear or not depending on who is asking.
|
|
239
|
+
*
|
|
240
|
+
* There is no matching `grantor` field: you only ever see grants you made or
|
|
241
|
+
* received, so the other side is either you or the grantee.
|
|
242
|
+
*/
|
|
243
|
+
readonly grantee: User;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Arguments for {@link MovieAddonGrantsNamespace.create}.
|
|
247
|
+
*
|
|
248
|
+
* Pass exactly one target. Both or neither is
|
|
249
|
+
* `400 "Grant must target one addon or one group"`.
|
|
250
|
+
*/
|
|
251
|
+
export interface CreateMovieAddonGrantInput {
|
|
252
|
+
/** Share one addon. Mutually exclusive with {@link movie_addon_group_id}. */
|
|
253
|
+
readonly movie_addon_id?: Id | null;
|
|
254
|
+
/** Share a whole group, present and future contents. */
|
|
255
|
+
readonly movie_addon_group_id?: Id | null;
|
|
256
|
+
/** Who to share with. Must not be yourself. */
|
|
257
|
+
readonly grantee_id: Id;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Filters for {@link MovieAddonGrantsNamespace.list}.
|
|
261
|
+
*
|
|
262
|
+
* This controller declares no `search_params` at all, so the allowlist is only
|
|
263
|
+
* the framework default: `id`, `created_at`, `updated_at`. You CANNOT ask the
|
|
264
|
+
* server for "grants I made" versus "grants I received", nor for the grants on
|
|
265
|
+
* one addon - those are `400`s. Filter on `grantor_id` / `grantee_id` /
|
|
266
|
+
* `movie_addon_id` client-side after listing.
|
|
267
|
+
*/
|
|
268
|
+
export interface ListMovieAddonGrantsParams extends PageParams {
|
|
269
|
+
/** Exact match on the primary key. */
|
|
270
|
+
readonly id?: Id;
|
|
271
|
+
}
|
|
272
|
+
/** The manual kind: a collection the user created and may rename or delete. */
|
|
273
|
+
export declare const MOVIE_COLLECTION_MANUAL_KIND = "manual";
|
|
274
|
+
/**
|
|
275
|
+
* The one system kind. There is exactly one favourites collection per user,
|
|
276
|
+
* enforced by a partial unique index on `(user_id, kind) WHERE kind <> 'manual'`.
|
|
277
|
+
*/
|
|
278
|
+
export declare const MOVIE_COLLECTION_FAVORITES_KIND = "favorites";
|
|
279
|
+
/** Every kind `MovieCollection::KINDS` allows. Anything else is a `400`. */
|
|
280
|
+
export declare const MOVIE_COLLECTION_KINDS: readonly ["manual", "favorites"];
|
|
281
|
+
/** One of {@link MOVIE_COLLECTION_KINDS}. */
|
|
282
|
+
export type MovieCollectionKind = (typeof MOVIE_COLLECTION_KINDS)[number];
|
|
283
|
+
/** Longest name `MovieAddonGroup` accepts. Over it is a `400`. */
|
|
284
|
+
export declare const MOVIE_ADDON_GROUP_NAME_MAX_LENGTH = 80;
|
|
285
|
+
/**
|
|
286
|
+
* A user's list of titles: the auto-created favourites row, or a playlist they
|
|
287
|
+
* made by hand.
|
|
288
|
+
*/
|
|
289
|
+
export interface MovieCollection extends BaseRecord {
|
|
290
|
+
readonly user_id: Id;
|
|
291
|
+
readonly name: string;
|
|
292
|
+
readonly kind: MovieCollectionKind;
|
|
293
|
+
/**
|
|
294
|
+
* Sort key inside the sidebar. Favourites is minted at `-1` so it sorts
|
|
295
|
+
* first; manual collections start at `max + 1`.
|
|
296
|
+
*
|
|
297
|
+
* The index has NO order of its own, so this only sorts if you ask for it -
|
|
298
|
+
* pass `order: "position:asc"`.
|
|
299
|
+
*/
|
|
300
|
+
readonly position: number;
|
|
301
|
+
/**
|
|
302
|
+
* `true` for the favourites row. Mirrors `kind != "manual"`, so it is `true`
|
|
303
|
+
* for any future system kind too. Use {@link isSystemMovieCollection}.
|
|
304
|
+
*/
|
|
305
|
+
readonly system: boolean;
|
|
306
|
+
/**
|
|
307
|
+
* How many items are in it. Counted from the association the controller
|
|
308
|
+
* preloads, so it is exact and costs no extra query - but it is a snapshot,
|
|
309
|
+
* and adding an item does not refresh the collection row you are holding.
|
|
310
|
+
*/
|
|
311
|
+
readonly items_count: number;
|
|
312
|
+
}
|
|
313
|
+
/** One title filed into a collection. */
|
|
314
|
+
export interface MovieCollectionItem extends BaseRecord {
|
|
315
|
+
readonly movie_collection_id: Id;
|
|
316
|
+
/** `"movie"`, `"series"`, whatever the addon called it. */
|
|
317
|
+
readonly movie_type: MovieType;
|
|
318
|
+
/** The addon's own id for the title, e.g. an IMDb id. Not a database id. */
|
|
319
|
+
readonly movie_id: string;
|
|
320
|
+
/** Denormalised metadata, so a grid renders without hitting the addon. */
|
|
321
|
+
readonly name: string | null;
|
|
322
|
+
readonly poster: string | null;
|
|
323
|
+
readonly background: string | null;
|
|
324
|
+
readonly release_info: string | null;
|
|
325
|
+
/** Sort key inside the collection, dense from `0`. See {@link MovieCollectionsNamespace.reorder}. */
|
|
326
|
+
readonly position: number;
|
|
327
|
+
}
|
|
328
|
+
/** Arguments for {@link MovieCollectionsNamespace.create}. */
|
|
329
|
+
export interface CreateMovieCollectionInput {
|
|
330
|
+
/**
|
|
331
|
+
* Required. It is the ONLY field create reads: `before_create` overwrites
|
|
332
|
+
* `user`, forces `kind` to `"manual"` and computes `position` as
|
|
333
|
+
* `max(position) + 1`, so passing a kind or a position is silently ignored
|
|
334
|
+
* rather than rejected. There is no way to mint a second system collection.
|
|
335
|
+
*/
|
|
336
|
+
readonly name: string;
|
|
337
|
+
}
|
|
338
|
+
/** Arguments for {@link MovieCollectionsNamespace.update}. */
|
|
339
|
+
export interface UpdateMovieCollectionInput {
|
|
340
|
+
readonly name?: string;
|
|
341
|
+
/**
|
|
342
|
+
* Sidebar order. Nothing normalises it: two collections can hold the same
|
|
343
|
+
* position and the server will not complain, so the client owns keeping the
|
|
344
|
+
* sequence sane.
|
|
345
|
+
*/
|
|
346
|
+
readonly position?: number;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Filters for {@link MovieCollectionsNamespace.list}.
|
|
350
|
+
*
|
|
351
|
+
* Allowlist: `id`, `name`, `kind`, `created_at`, `updated_at`. Anything else is
|
|
352
|
+
* `400 "Unknown search filter: ..."`.
|
|
353
|
+
*/
|
|
354
|
+
export interface ListMovieCollectionsParams extends PageParams {
|
|
355
|
+
/** Exact match, or `IN (...)` when given an array. */
|
|
356
|
+
readonly id?: Id | readonly Id[];
|
|
357
|
+
/**
|
|
358
|
+
* Partial, accent-folded, case-insensitive match - the `LIKE` a search box
|
|
359
|
+
* wants. NOT equality: `"fav"` matches `"Favoritos"`.
|
|
360
|
+
*/
|
|
361
|
+
readonly name?: string;
|
|
362
|
+
/** Exact match. `"favorites"` finds the one system row. */
|
|
363
|
+
readonly kind?: MovieCollectionKind;
|
|
364
|
+
}
|
|
365
|
+
/** Arguments for {@link MovieCollectionItemsNamespace.create}. */
|
|
366
|
+
export interface CreateMovieCollectionItemInput {
|
|
367
|
+
readonly movie_collection_id: Id;
|
|
368
|
+
readonly movie_type: MovieType;
|
|
369
|
+
/** The addon's id for the title. Required, and part of the uniqueness key. */
|
|
370
|
+
readonly movie_id: string;
|
|
371
|
+
readonly name?: string | null;
|
|
372
|
+
readonly poster?: string | null;
|
|
373
|
+
readonly background?: string | null;
|
|
374
|
+
readonly release_info?: string | null;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Filters for {@link MovieCollectionItemsNamespace.list}.
|
|
378
|
+
*
|
|
379
|
+
* Allowlist: `id`, `movie_collection_id`, `movie_type`, `movie_id`,
|
|
380
|
+
* `position`, `created_at`, `updated_at`.
|
|
381
|
+
*/
|
|
382
|
+
export interface ListMovieCollectionItemsParams extends PageParams {
|
|
383
|
+
/** Exact match, or `IN (...)` when given an array. */
|
|
384
|
+
readonly id?: Id | readonly Id[];
|
|
385
|
+
/**
|
|
386
|
+
* Exact match, or `IN (...)` for several collections at once. Almost always
|
|
387
|
+
* what you want: the bare index returns the items of EVERY collection the
|
|
388
|
+
* caller owns, interleaved.
|
|
389
|
+
*/
|
|
390
|
+
readonly collectionId?: Id | readonly Id[];
|
|
391
|
+
/** Exact match. */
|
|
392
|
+
readonly movieType?: MovieType;
|
|
393
|
+
/** Exact match on the addon's title id. */
|
|
394
|
+
readonly movieId?: string;
|
|
395
|
+
/** Exact match on the sort key. */
|
|
396
|
+
readonly position?: number;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Fraction of the runtime that counts as watched when the server derives
|
|
400
|
+
* `finished` itself. Mirrors `MovieWatchProgress::FINISHED_THRESHOLD`.
|
|
401
|
+
*/
|
|
402
|
+
export declare const MOVIE_WATCH_FINISHED_THRESHOLD = 0.95;
|
|
403
|
+
/**
|
|
404
|
+
* Rows `POST /movie_watch_progresses/bulk` will accept in one call. Mirrors
|
|
405
|
+
* `MovieWatchProgressesController::BULK_LIMIT`.
|
|
406
|
+
*
|
|
407
|
+
* The server does `Array(params[:items]).first(200)`: entries past the limit
|
|
408
|
+
* are dropped in SILENCE and the call still answers `200` with the 200 rows it
|
|
409
|
+
* did save, so a client marking a 300-episode series watched would believe it
|
|
410
|
+
* succeeded. {@link MovieWatchProgressesNamespace.saveMany} raises instead.
|
|
411
|
+
*/
|
|
412
|
+
export declare const MOVIE_WATCH_BULK_LIMIT = 200;
|
|
413
|
+
/**
|
|
414
|
+
* Rows `GET /movie_watch_progresses` returns, at most. Hard-coded `limit(500)`
|
|
415
|
+
* in the controller, with no paging and no way to reach row 501 - see
|
|
416
|
+
* {@link MovieWatchProgressesNamespace.list}.
|
|
417
|
+
*/
|
|
418
|
+
export declare const MOVIE_WATCH_LIST_LIMIT = 500;
|
|
419
|
+
/** One title-or-episode the user has started, and how far in they got. */
|
|
420
|
+
export interface MovieWatchProgress extends BaseRecord {
|
|
421
|
+
readonly user_id: Id;
|
|
422
|
+
readonly movie_type: MovieType;
|
|
423
|
+
/** The addon's id for the TITLE. A series shares it across every episode. */
|
|
424
|
+
readonly movie_id: string;
|
|
425
|
+
/**
|
|
426
|
+
* The addon's id for the specific playable. For a film this is usually the
|
|
427
|
+
* same string as `movie_id`; for a series it is the episode.
|
|
428
|
+
*
|
|
429
|
+
* `(user_id, movie_id, video_id)` is the row's identity and carries a unique
|
|
430
|
+
* index.
|
|
431
|
+
*/
|
|
432
|
+
readonly video_id: string;
|
|
433
|
+
readonly season: number | null;
|
|
434
|
+
readonly episode: number | null;
|
|
435
|
+
readonly name: string | null;
|
|
436
|
+
readonly episode_title: string | null;
|
|
437
|
+
readonly poster: string | null;
|
|
438
|
+
/** Seconds into the playable. A float, `NOT NULL DEFAULT 0.0`. */
|
|
439
|
+
readonly position: number;
|
|
440
|
+
/** Runtime in seconds, as the player measured it. `0` when unknown. */
|
|
441
|
+
readonly duration: number;
|
|
442
|
+
/** See {@link MovieWatchProgressInput.finished} for how this gets its value. */
|
|
443
|
+
readonly finished: boolean;
|
|
444
|
+
/** What "Continuar a ver" sorts on, descending. */
|
|
445
|
+
readonly last_watched_at: Timestamp;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* One row to upsert, through {@link MovieWatchProgressesNamespace.save} or
|
|
449
|
+
* {@link MovieWatchProgressesNamespace.saveMany}.
|
|
450
|
+
*
|
|
451
|
+
* `movie_id`, `video_id` and `movie_type` are checked up front by the
|
|
452
|
+
* controller and a missing one is
|
|
453
|
+
* `400 "movie_id, video_id, movie_type are required"`. Everything else is
|
|
454
|
+
* optional, but read the notes on `finished` and `last_watched_at` before
|
|
455
|
+
* leaving them out: both are cases where omitting the field does something
|
|
456
|
+
* other than "leave it as it was".
|
|
457
|
+
*/
|
|
458
|
+
export interface MovieWatchProgressInput {
|
|
459
|
+
readonly movie_type: MovieType;
|
|
460
|
+
/** Identity, with `video_id`. Changing `movie_type` does NOT make a new row. */
|
|
461
|
+
readonly movie_id: string;
|
|
462
|
+
/** Identity, with `movie_id`. */
|
|
463
|
+
readonly video_id: string;
|
|
464
|
+
readonly season?: number | null;
|
|
465
|
+
readonly episode?: number | null;
|
|
466
|
+
readonly name?: string | null;
|
|
467
|
+
readonly episode_title?: string | null;
|
|
468
|
+
readonly poster?: string | null;
|
|
469
|
+
/** Seconds into the playable. Negative is `400 "Position must be greater than or equal to 0"`. */
|
|
470
|
+
readonly position?: number;
|
|
471
|
+
/** Runtime in seconds. Negative is a `400` the same way. */
|
|
472
|
+
readonly duration?: number;
|
|
473
|
+
/**
|
|
474
|
+
* Three states, not two.
|
|
475
|
+
*
|
|
476
|
+
* - **omitted** - the server derives it:
|
|
477
|
+
* `finished = position >= duration * 0.95`, and if `duration <= 0` it
|
|
478
|
+
* leaves the stored flag ALONE. This is what a playback tick should send.
|
|
479
|
+
* - **`true` / `false`** - the user said so. `upsert_for` sets the model's
|
|
480
|
+
* `finished_given` flag, which makes `set_finished` return before its
|
|
481
|
+
* `duration <= 0` guard, so the value is written as given.
|
|
482
|
+
* - **`null`** - deleted by `progress_params` before it reaches the model
|
|
483
|
+
* (`permitted.delete(:finished) if permitted[:finished].nil?`) and
|
|
484
|
+
* therefore identical to omitting it.
|
|
485
|
+
*
|
|
486
|
+
* That last branch is the bug that was fixed here. "Marcar como nao visto"
|
|
487
|
+
* sends `position: 0, duration: 0`, which used to hit the `duration <= 0`
|
|
488
|
+
* guard and leave `finished` true forever. Sending an explicit `false` is
|
|
489
|
+
* what makes it stick - sending `null`, or leaving the key out, still does
|
|
490
|
+
* nothing at all. {@link MovieWatchProgressesNamespace.setWatched} spells it
|
|
491
|
+
* out so you cannot get this wrong by accident.
|
|
492
|
+
*
|
|
493
|
+
* `finished_given` is a plain `attr_accessor`, not a column, so it only
|
|
494
|
+
* pins the value for THAT save. The next tick that omits `finished` goes
|
|
495
|
+
* back to deriving it from the position.
|
|
496
|
+
*/
|
|
497
|
+
readonly finished?: boolean;
|
|
498
|
+
/**
|
|
499
|
+
* When the user last watched, ISO-8601.
|
|
500
|
+
*
|
|
501
|
+
* SEND IT ON EVERY CALL. The model only defaults it with
|
|
502
|
+
* `self.last_watched_at ||= Time.current`, which is a no-op on an existing
|
|
503
|
+
* row - so an upsert that omits it keeps whatever timestamp was there when
|
|
504
|
+
* the row was first created. The Continue Watching list is
|
|
505
|
+
* `order(last_watched_at: :desc)`, so omitting this pins the title where it
|
|
506
|
+
* first appeared and it never moves back to the front. `new Date().toISOString()`
|
|
507
|
+
* at the call site is the whole fix.
|
|
508
|
+
*/
|
|
509
|
+
readonly last_watched_at?: Timestamp;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Whether a collection is the server-managed favourites row.
|
|
513
|
+
*
|
|
514
|
+
* Prefer this over `collection.kind === "favorites"`: the server's own test is
|
|
515
|
+
* `kind != "manual"`, so a future system kind reads as system there and would
|
|
516
|
+
* read as manual in a hand-written equality check. The blueprint already
|
|
517
|
+
* computes it; this just keeps the test in one place.
|
|
518
|
+
*/
|
|
519
|
+
export declare function isSystemMovieCollection(collection: Pick<MovieCollection, "kind">): boolean;
|
|
520
|
+
/**
|
|
521
|
+
* The same arithmetic `MovieWatchProgress#set_finished` uses, for a client that
|
|
522
|
+
* wants to render a "watched" tick before the round trip lands.
|
|
523
|
+
*
|
|
524
|
+
* Returns `null` - not `false` - when `duration` is zero or negative, because
|
|
525
|
+
* that is precisely the case where the server declines to decide and leaves the
|
|
526
|
+
* stored flag untouched. Treating that as `false` is how an optimistic UI ends
|
|
527
|
+
* up un-ticking something the server still considers watched.
|
|
528
|
+
*/
|
|
529
|
+
export declare function movieWatchFinished(position: number, duration: number): boolean | null;
|
|
530
|
+
/**
|
|
531
|
+
* The `movies.addons.groups` namespace: named folders of addons, which double
|
|
532
|
+
* as sharing units.
|
|
533
|
+
*
|
|
534
|
+
* There is no `show` route. Read one out of {@link list}.
|
|
535
|
+
*/
|
|
536
|
+
export declare class MovieAddonGroupsNamespace extends Resource {
|
|
537
|
+
/**
|
|
538
|
+
* `GET /movie_addon_groups` - the caller's own groups.
|
|
539
|
+
*
|
|
540
|
+
* `viewable_by` is `where(user: user)` with no grant clause, so a group
|
|
541
|
+
* somebody shared with you never appears here even though its addons do.
|
|
542
|
+
* The shared addons arrive from {@link MovieAddonsNamespace.list} carrying a
|
|
543
|
+
* `movie_addon_group_id` you cannot resolve; render them under a single
|
|
544
|
+
* "shared with me" heading rather than trying to look the group up.
|
|
545
|
+
*
|
|
546
|
+
* The relation has NO order of its own, so pass `order` (typically
|
|
547
|
+
* `"name:asc"` or `"created_at:desc"`) - paging an unordered relation can
|
|
548
|
+
* repeat and drop rows.
|
|
549
|
+
*
|
|
550
|
+
* @throws {OmsApiError} 401 for an anonymous caller, 403 for an OAuth token.
|
|
551
|
+
* @throws {OmsApiError} 400 `"Unknown search filter: ..."` for any filter
|
|
552
|
+
* beyond `id`, `created_at` and `updated_at`.
|
|
553
|
+
*/
|
|
554
|
+
list(params?: ListMovieAddonGroupsParams, options?: RequestOptions): Promise<Paginated<MovieAddonGroup>>;
|
|
555
|
+
/**
|
|
556
|
+
* `POST /movie_addon_groups` - creates a group owned by the caller. `201`.
|
|
557
|
+
*
|
|
558
|
+
* `name` is the only writable field; the owner is forced to `Current.user` in
|
|
559
|
+
* a `before_validation`, so there is no way to create one for somebody else.
|
|
560
|
+
*
|
|
561
|
+
* Names are NOT unique: two groups called "Filmes" are allowed and will look
|
|
562
|
+
* identical in a picker. De-duplicate client-side if that matters.
|
|
563
|
+
*
|
|
564
|
+
* @throws {OmsApiError} 400 `"Name can't be blank"` or
|
|
565
|
+
* `"Name is too long (maximum is 80 characters)"`.
|
|
566
|
+
*/
|
|
567
|
+
create(input: CreateMovieAddonGroupInput | string, options?: RequestOptions): Promise<MovieAddonGroup>;
|
|
568
|
+
/**
|
|
569
|
+
* `PATCH /movie_addon_groups/:id` - renames a group. `200`.
|
|
570
|
+
*
|
|
571
|
+
* Unlike its sibling {@link MovieAddonsNamespace.update}, this one is a real
|
|
572
|
+
* partial update: `update_params :name` permits exactly one key and nothing
|
|
573
|
+
* is assigned behind your back.
|
|
574
|
+
*
|
|
575
|
+
* @throws {OmsApiError} 404 `"Resource not found"` for an id that is not
|
|
576
|
+
* yours - ownership is applied by the lookup scope, so somebody else's
|
|
577
|
+
* group is indistinguishable from a group that does not exist.
|
|
578
|
+
*/
|
|
579
|
+
update(id: Id, name: string, options?: RequestOptions): Promise<MovieAddonGroup>;
|
|
580
|
+
/**
|
|
581
|
+
* `DELETE /movie_addon_groups/:id` - `204`, empty body.
|
|
582
|
+
*
|
|
583
|
+
* Two different cascades, and only one of them destroys anything:
|
|
584
|
+
*
|
|
585
|
+
* - the addons inside are `dependent: :nullify`, so they SURVIVE and become
|
|
586
|
+
* ungrouped;
|
|
587
|
+
* - the grants on the group are `dependent: :destroy`, so every share made
|
|
588
|
+
* through this group is revoked. People who could see those addons stop
|
|
589
|
+
* seeing them, with no notification. Direct grants on the individual
|
|
590
|
+
* addons are untouched.
|
|
591
|
+
*
|
|
592
|
+
* @throws {OmsApiError} 404 for an id that is not yours.
|
|
593
|
+
*/
|
|
594
|
+
delete(id: Id, options?: RequestOptions): Promise<void>;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* The `movies.addons.grants` namespace: sharing an addon, or a whole group,
|
|
598
|
+
* with one other user.
|
|
599
|
+
*
|
|
600
|
+
* Grants are create-and-delete only; there is no update route and no `show`.
|
|
601
|
+
* To change who can see what, delete the grant and make a new one.
|
|
602
|
+
*/
|
|
603
|
+
export declare class MovieAddonGrantsNamespace extends Resource {
|
|
604
|
+
/**
|
|
605
|
+
* `GET /movie_addon_grants` - every grant the caller made OR received.
|
|
606
|
+
*
|
|
607
|
+
* Both directions come back in one undifferentiated list
|
|
608
|
+
* (`where(grantor: user).or(where(grantee: user))`) and the index accepts no
|
|
609
|
+
* filter to separate them, so split on `grantor_id === myUserId` yourself.
|
|
610
|
+
*
|
|
611
|
+
* The relation has no order of its own; pass `order: "created_at:desc"`.
|
|
612
|
+
*
|
|
613
|
+
* @throws {OmsApiError} 400 `"Unknown search filter: ..."` for anything
|
|
614
|
+
* beyond `id`, `created_at` and `updated_at`. See
|
|
615
|
+
* {@link ListMovieAddonGrantsParams}.
|
|
616
|
+
*/
|
|
617
|
+
list(params?: ListMovieAddonGrantsParams, options?: RequestOptions): Promise<Paginated<MovieAddonGrant>>;
|
|
618
|
+
/**
|
|
619
|
+
* `POST /movie_addon_grants` - shares one addon, or one group, with one user.
|
|
620
|
+
* `201`.
|
|
621
|
+
*
|
|
622
|
+
* The grantor is always the caller: it is forced in a `before_validation` and
|
|
623
|
+
* re-checked on save, so there is no way to make a grant in somebody else's
|
|
624
|
+
* name.
|
|
625
|
+
*
|
|
626
|
+
* ## Failure modes, and which status each one is
|
|
627
|
+
*
|
|
628
|
+
* - **both targets, or neither** - `400 "Grant must target one addon or one
|
|
629
|
+
* group"`. Caught here before the request goes out.
|
|
630
|
+
* - **a target you do not own, or granting to YOURSELF** - `401 "You are not
|
|
631
|
+
* authorized to create this resource"`. Not a `400` and not a `403`:
|
|
632
|
+
* `creatable_by?` runs inside `CrudActions#create`, so a business-rule
|
|
633
|
+
* violation comes back wearing an authentication status. A generic error
|
|
634
|
+
* handler that logs the user out on `401` will do exactly that here.
|
|
635
|
+
* - **granting the same target to the same person twice** - this is the one
|
|
636
|
+
* to be careful with. There is a partial unique index on
|
|
637
|
+
* `(movie_addon_id, grantee_id)` and another on
|
|
638
|
+
* `(movie_addon_group_id, grantee_id)`, and there is NO matching
|
|
639
|
+
* `validates :uniqueness` on the model, so the duplicate is not a tidy
|
|
640
|
+
* `400`: it raises `ActiveRecord::RecordNotUnique` out of `save`, lands in
|
|
641
|
+
* the global rescue, answers **500** and fires a Discord error alert. List
|
|
642
|
+
* the existing grants and check before you create, and do not put this call
|
|
643
|
+
* behind a blind retry.
|
|
644
|
+
*
|
|
645
|
+
* Retries are off by default for a POST anyway (the transport only replays
|
|
646
|
+
* safe methods unless you opt in); do not opt in here.
|
|
647
|
+
*
|
|
648
|
+
* @throws {OmsError} `invalid_request` when the target count is not exactly
|
|
649
|
+
* one, or `grantee_id` is blank.
|
|
650
|
+
*/
|
|
651
|
+
create(input: CreateMovieAddonGrantInput, options?: RequestOptions): Promise<MovieAddonGrant>;
|
|
652
|
+
/**
|
|
653
|
+
* `DELETE /movie_addon_grants/:id` - revokes a share. `204`, empty body.
|
|
654
|
+
*
|
|
655
|
+
* Only the GRANTOR may revoke: `destroyable_by?` is `grantor == user`. The
|
|
656
|
+
* grantee can see the grant in {@link list} but deleting it is
|
|
657
|
+
* `401 "You are not authorized to destroy this resource"` - there is no
|
|
658
|
+
* "leave this share" for the receiving side.
|
|
659
|
+
*
|
|
660
|
+
* @throws {OmsApiError} 404 for a grant that is neither yours nor shared
|
|
661
|
+
* with you, 401 when you are the grantee rather than the grantor.
|
|
662
|
+
*/
|
|
663
|
+
delete(id: Id, options?: RequestOptions): Promise<void>;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* The `movies.addons` namespace: installed Stremio addons, plus the groups and
|
|
667
|
+
* grants that share them.
|
|
668
|
+
*
|
|
669
|
+
* There is no `show` route on `/movie_addons`; {@link list} is how you read
|
|
670
|
+
* one.
|
|
671
|
+
*/
|
|
672
|
+
export declare class MovieAddonsNamespace extends Resource {
|
|
673
|
+
/** Named folders of addons, which are also the unit of sharing. */
|
|
674
|
+
readonly groups: MovieAddonGroupsNamespace;
|
|
675
|
+
/** Shares of an addon or a group with another user. */
|
|
676
|
+
readonly grants: MovieAddonGrantsNamespace;
|
|
677
|
+
constructor(http: ApiClient);
|
|
678
|
+
/**
|
|
679
|
+
* `GET /movie_addons` - the caller's addons AND every addon shared with them.
|
|
680
|
+
*
|
|
681
|
+
* `viewable_by` is a three-way `OR`: rows you own, rows granted to you
|
|
682
|
+
* directly, and rows whose group was granted to you. That last arm is why a
|
|
683
|
+
* grant on a group covers addons added to it later. The result is
|
|
684
|
+
* `.distinct`, so an addon shared both ways still appears once.
|
|
685
|
+
*
|
|
686
|
+
* Read {@link MovieAddon.shared} to tell the two kinds apart. Everything is
|
|
687
|
+
* read-only for a shared row.
|
|
688
|
+
*
|
|
689
|
+
* The relation has NO default order. Always pass `order` - `"created_at:desc"`
|
|
690
|
+
* matches the web app's "most recently installed first" - because paging an
|
|
691
|
+
* unordered Postgres relation can repeat and drop rows between pages.
|
|
692
|
+
*
|
|
693
|
+
* **Array filters are silently ignored on this index.** `search_params` here
|
|
694
|
+
* declares only scalars, so `id` given as an array is dropped by `permit`
|
|
695
|
+
* without a `400` and you get the UNFILTERED list back. The SDK only accepts
|
|
696
|
+
* a scalar for that reason. Unknown filter KEYS do fail closed with a `400`.
|
|
697
|
+
*
|
|
698
|
+
* This index emits an `ETag` and can answer `304`. Do not hand-write an
|
|
699
|
+
* `If-None-Match` header: the transport treats a bare `304` as a failure.
|
|
700
|
+
*
|
|
701
|
+
* @throws {OmsApiError} 401 for an anonymous caller, 403 for an OAuth token.
|
|
702
|
+
*/
|
|
703
|
+
list(params?: ListMovieAddonsParams, options?: RequestOptions): Promise<Paginated<MovieAddon>>;
|
|
704
|
+
/**
|
|
705
|
+
* `POST /movie_addons` - installs an addon, or re-installs one you already
|
|
706
|
+
* have. **`201` either way.**
|
|
707
|
+
*
|
|
708
|
+
* The controller does `find_or_initialize_by(user: Current.user,
|
|
709
|
+
* manifest_url: ...)` before the generic create runs, so posting a manifest
|
|
710
|
+
* URL you already installed UPDATES that row - refreshing `manifest_json` and
|
|
711
|
+
* reassigning `movie_addon_group_id` - instead of colliding with the
|
|
712
|
+
* `(user_id, manifest_url)` unique index. The status stays `201` and the `id`
|
|
713
|
+
* comes back unchanged, so `201` here does not mean "new row"; compare
|
|
714
|
+
* `created_at` if you need to know.
|
|
715
|
+
*
|
|
716
|
+
* The practical consequence is the good one: reinstalling is idempotent and
|
|
717
|
+
* the app can re-post its whole addon list on boot. The trap is the other
|
|
718
|
+
* side of it - re-posting with `movie_addon_group_id` omitted leaves the
|
|
719
|
+
* group as it was (the key is only assigned when present in `params.permit`),
|
|
720
|
+
* while re-posting it as `null` clears the group.
|
|
721
|
+
*
|
|
722
|
+
* `manifest_json` is stored with `to_unsafe_h`: no key is validated, no key
|
|
723
|
+
* is stripped, and whatever you send is what everyone the addon is shared
|
|
724
|
+
* with will later render. Fetch the manifest yourself and do not forward one
|
|
725
|
+
* a third party handed you unchecked.
|
|
726
|
+
*
|
|
727
|
+
* @throws {OmsApiError} 400 `"Manifest url must be a valid URL"`,
|
|
728
|
+
* `"Manifest json can't be blank"` (an empty object counts as blank), or
|
|
729
|
+
* `"Movie addon group must belong to addon owner"`.
|
|
730
|
+
*/
|
|
731
|
+
create(input: CreateMovieAddonInput, options?: RequestOptions): Promise<MovieAddon>;
|
|
732
|
+
/**
|
|
733
|
+
* `PATCH /movie_addons/:id` - `200`.
|
|
734
|
+
*
|
|
735
|
+
* **This is not a partial update, whatever the verb says.**
|
|
736
|
+
* `movie_addon_params` is shared between create and update and finishes with
|
|
737
|
+
* an unconditional `permitted[:manifest_json] = params[:manifest_json]`, so
|
|
738
|
+
* the key is always assigned - as `nil` when you did not send one. `nil` then
|
|
739
|
+
* fails `validates :manifest_json, presence: true` and the whole call is
|
|
740
|
+
* `400 "Manifest json can't be blank"`. Nothing is written; the row is fine.
|
|
741
|
+
* It simply cannot be patched without resending the manifest.
|
|
742
|
+
*
|
|
743
|
+
* That is a live divergence: the web frontend's
|
|
744
|
+
* `MovieAddonsService.update(id, { movie_addon_group_id })` sends the group
|
|
745
|
+
* alone and therefore 400s. {@link UpdateMovieAddonInput.manifest_json} is
|
|
746
|
+
* required here so the same mistake is a compile error, and
|
|
747
|
+
* {@link moveToGroup} carries the manifest across for you.
|
|
748
|
+
*
|
|
749
|
+
* Only the owner may update: a shared row is `401`.
|
|
750
|
+
*
|
|
751
|
+
* @throws {OmsError} `invalid_request` when `manifest_json` is missing or empty.
|
|
752
|
+
* @throws {OmsApiError} 404 for an id you cannot see, 401 for one you can see
|
|
753
|
+
* but do not own.
|
|
754
|
+
*/
|
|
755
|
+
update(id: Id, input: UpdateMovieAddonInput, options?: RequestOptions): Promise<MovieAddon>;
|
|
756
|
+
/**
|
|
757
|
+
* Files an addon under a group, or un-groups it with `null`.
|
|
758
|
+
*
|
|
759
|
+
* Sugar over {@link update} that exists purely because the endpoint demands
|
|
760
|
+
* the manifest back on every patch. Pass the {@link MovieAddon} you already
|
|
761
|
+
* hold and its `manifest_json` is resent unchanged; there is no extra request
|
|
762
|
+
* and no fetch of the manifest.
|
|
763
|
+
*
|
|
764
|
+
* Refuses a shared addon before the round trip: the server would answer
|
|
765
|
+
* `401`, and the message here says why.
|
|
766
|
+
*
|
|
767
|
+
* @throws {OmsError} `invalid_request` for an addon whose `shared` flag is set.
|
|
768
|
+
*/
|
|
769
|
+
moveToGroup(addon: MovieAddon, groupId: Id | null, options?: RequestOptions): Promise<MovieAddon>;
|
|
770
|
+
/**
|
|
771
|
+
* `DELETE /movie_addons/:id` - uninstalls. `204`, empty body.
|
|
772
|
+
*
|
|
773
|
+
* Owner only; uninstalling an addon somebody shared with you is `401`, and
|
|
774
|
+
* the way to lose one of those is for the grantor to revoke the grant.
|
|
775
|
+
*
|
|
776
|
+
* Every grant ON this addon is `dependent: :destroy`, so deleting it revokes
|
|
777
|
+
* the shares along with it. Grants that reached people through its GROUP are
|
|
778
|
+
* untouched - they belong to the group, which still exists.
|
|
779
|
+
*
|
|
780
|
+
* @throws {OmsApiError} 404 for an id you cannot see, 401 for a shared one.
|
|
781
|
+
*/
|
|
782
|
+
delete(id: Id, options?: RequestOptions): Promise<void>;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* The `movies.collections.items` namespace: the titles filed into a collection.
|
|
786
|
+
*
|
|
787
|
+
* Index and create and delete; there is no `show` and no `update`. To change a
|
|
788
|
+
* title's stored metadata, {@link create} it again - it upserts.
|
|
789
|
+
*/
|
|
790
|
+
export declare class MovieCollectionItemsNamespace extends Resource {
|
|
791
|
+
/**
|
|
792
|
+
* `GET /movie_collection_items` - items across the caller's collections.
|
|
793
|
+
*
|
|
794
|
+
* **Pass `collectionId` unless you really mean everything.** With no filter
|
|
795
|
+
* this returns the items of EVERY collection the caller owns, and because
|
|
796
|
+
* `MovieCollectionItem` carries `default_scope { order(position: :asc) }`
|
|
797
|
+
* they come back interleaved by position rather than grouped by collection -
|
|
798
|
+
* position 0 of each list, then position 1 of each, and so on. Grouping that
|
|
799
|
+
* back together client-side works but reads like a bug when you first see it.
|
|
800
|
+
*
|
|
801
|
+
* `collectionId` accepts an array, which becomes `IN (...)`: one request for
|
|
802
|
+
* the three lists a screen shows.
|
|
803
|
+
*
|
|
804
|
+
* The default order is `position:asc` and it is the useful one, so leave
|
|
805
|
+
* `order` alone unless you want something else. Note that passing `order`
|
|
806
|
+
* REPLACES the default (`QueryModifier` uses `reorder`), it does not add to
|
|
807
|
+
* it, so `order: "created_at:desc"` loses the position ordering entirely.
|
|
808
|
+
*
|
|
809
|
+
* @throws {OmsApiError} 400 `"Unknown search filter: ..."` outside the
|
|
810
|
+
* allowlist in {@link ListMovieCollectionItemsParams}.
|
|
811
|
+
*/
|
|
812
|
+
list(params?: ListMovieCollectionItemsParams, options?: RequestOptions): Promise<Paginated<MovieCollectionItem>>;
|
|
813
|
+
/**
|
|
814
|
+
* `POST /movie_collection_items` - adds a title to a collection, or refreshes
|
|
815
|
+
* the one already there. **`201` either way.**
|
|
816
|
+
*
|
|
817
|
+
* The controller does `find_or_initialize_by(movie_collection_id, movie_type,
|
|
818
|
+
* movie_id)` first, so adding the same title twice is a no-op-with-an-update
|
|
819
|
+
* rather than a `400` off the unique index. This is deliberate: the heart
|
|
820
|
+
* button and the "add to list" dialog both fire blind, holding only the
|
|
821
|
+
* collection they already loaded. The upshot is that this call is safe to
|
|
822
|
+
* repeat and safe to fire optimistically.
|
|
823
|
+
*
|
|
824
|
+
* Two consequences worth knowing:
|
|
825
|
+
*
|
|
826
|
+
* - `position` is only computed for a NEW row (`max(position) + 1`, starting
|
|
827
|
+
* at `0`). Re-adding an existing title keeps its place in the list rather
|
|
828
|
+
* than moving it to the end.
|
|
829
|
+
* - the denormalised metadata (`name`, `poster`, `background`,
|
|
830
|
+
* `release_info`) IS overwritten every time, so re-posting is how you
|
|
831
|
+
* refresh a poster that the addon has since changed. Sending `null` for one
|
|
832
|
+
* clears it; omitting the key leaves the stored value alone.
|
|
833
|
+
*
|
|
834
|
+
* A `movie_collection_id` that does not exist, or belongs to somebody else,
|
|
835
|
+
* is `401 "You are not authorized to create this resource"` and NOT a `404`:
|
|
836
|
+
* `creatable_by?` reads `movie_collection&.user == user`, and a missing
|
|
837
|
+
* collection makes that `nil == user`, which is false. Do not read that `401`
|
|
838
|
+
* as "the session expired".
|
|
839
|
+
*
|
|
840
|
+
* Adding to the favourites collection is allowed - the system flag blocks
|
|
841
|
+
* renaming, reordering and deleting the COLLECTION, not writing items into
|
|
842
|
+
* it. That is how the heart button works.
|
|
843
|
+
*
|
|
844
|
+
* @throws {OmsError} `invalid_request` when the collection id, type or movie
|
|
845
|
+
* id is blank.
|
|
846
|
+
* @throws {OmsApiError} 400 `"Movie type can't be blank"` /
|
|
847
|
+
* `"Movie can't be blank"`, 401 for a collection that is not yours.
|
|
848
|
+
*/
|
|
849
|
+
create(input: CreateMovieCollectionItemInput, options?: RequestOptions): Promise<MovieCollectionItem>;
|
|
850
|
+
/**
|
|
851
|
+
* `DELETE /movie_collection_items/:id` - `204`, empty body.
|
|
852
|
+
*
|
|
853
|
+
* The id is the ITEM's primary key, not the `movie_id` the addon uses. If
|
|
854
|
+
* all you hold is a title, {@link list} it with `collectionId` and `movieId`
|
|
855
|
+
* first, or keep the item rows the collection screen already loaded.
|
|
856
|
+
*
|
|
857
|
+
* Removing leaves a gap in `position`: nothing renumbers the survivors, and
|
|
858
|
+
* the next {@link create} takes `max + 1`, so positions drift sparse over
|
|
859
|
+
* time. Only {@link MovieCollectionsNamespace.reorder} makes them dense
|
|
860
|
+
* again. Nothing depends on them being dense.
|
|
861
|
+
*
|
|
862
|
+
* @throws {OmsApiError} 404 for an item outside your collections.
|
|
863
|
+
*/
|
|
864
|
+
delete(id: Id, options?: RequestOptions): Promise<void>;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* The `movies.collections` namespace: the favourites row plus whatever lists
|
|
868
|
+
* the user built by hand.
|
|
869
|
+
*/
|
|
870
|
+
export declare class MovieCollectionsNamespace extends Resource {
|
|
871
|
+
/** The titles inside a collection. */
|
|
872
|
+
readonly items: MovieCollectionItemsNamespace;
|
|
873
|
+
constructor(http: ApiClient);
|
|
874
|
+
/**
|
|
875
|
+
* `GET /movie_collections` - the caller's collections, each with its
|
|
876
|
+
* `items_count`.
|
|
877
|
+
*
|
|
878
|
+
* **This call has a side effect, and it is the only one that does.**
|
|
879
|
+
* `listing_scope` runs `MovieCollection.favorites_for(Current.user)` before
|
|
880
|
+
* anything else, which `find_or_create_by!`s the "Favoritos" row (kind
|
|
881
|
+
* `favorites`, position `-1`). The index is the only place that knows the
|
|
882
|
+
* user has opened the movies app, so it is where the row gets minted. Two
|
|
883
|
+
* consequences: a brand new account's first listing WRITES to the database,
|
|
884
|
+
* and there is no other way to make favourites exist - a client that goes
|
|
885
|
+
* straight to the heart button without ever listing has no collection to put
|
|
886
|
+
* the title in. List first.
|
|
887
|
+
*
|
|
888
|
+
* A concurrent second tab racing the same first listing is handled: the
|
|
889
|
+
* partial unique index raises, the model rescues `RecordNotUnique` and reads
|
|
890
|
+
* the winner's row back.
|
|
891
|
+
*
|
|
892
|
+
* `items_count` is exact and free - the controller preloads
|
|
893
|
+
* `:movie_collection_items` so the blueprint counts a loaded array instead of
|
|
894
|
+
* firing a `COUNT` per row. {@link get} does not preload, so it costs one
|
|
895
|
+
* `COUNT` there. Neither is a snapshot you can trust after a write.
|
|
896
|
+
*
|
|
897
|
+
* No default order. Pass `order: "position:asc"` to get the sidebar's own
|
|
898
|
+
* order, which puts favourites first by virtue of its `-1`.
|
|
899
|
+
*
|
|
900
|
+
* @throws {OmsApiError} 401 for an anonymous caller, 403 for an OAuth token.
|
|
901
|
+
*/
|
|
902
|
+
list(params?: ListMovieCollectionsParams, options?: RequestOptions): Promise<Paginated<MovieCollection>>;
|
|
903
|
+
/**
|
|
904
|
+
* Reads the caller's favourites collection, creating it if this is the first
|
|
905
|
+
* time they have opened the app.
|
|
906
|
+
*
|
|
907
|
+
* A one-line convenience over {@link list} that exists because "get me the
|
|
908
|
+
* heart list" is the single most common reason to call the index, and
|
|
909
|
+
* because doing it by hand invites filtering on `kind` client-side after a
|
|
910
|
+
* listing that may have been paged.
|
|
911
|
+
*
|
|
912
|
+
* Resolves to `null` only if the server somehow answered without the row,
|
|
913
|
+
* which should not happen - the listing mints it.
|
|
914
|
+
*/
|
|
915
|
+
favorites(options?: RequestOptions): Promise<MovieCollection | null>;
|
|
916
|
+
/**
|
|
917
|
+
* `GET /movie_collections/:id` - one collection.
|
|
918
|
+
*
|
|
919
|
+
* The only `show` route in this whole namespace. It renders the `:extended`
|
|
920
|
+
* view, which for these blueprints is byte-identical to the default view the
|
|
921
|
+
* index returns: `ApplicationBlueprint` declares `view :extended do end`, and
|
|
922
|
+
* a Blueprinter view INHERITS the base fields and adds nothing here. So
|
|
923
|
+
* `show` is not a richer payload, only a single-row one.
|
|
924
|
+
*
|
|
925
|
+
* Unlike {@link list} it does NOT mint the favourites row.
|
|
926
|
+
*
|
|
927
|
+
* @throws {OmsApiError} 404 `"Resource not found"` for a collection that is
|
|
928
|
+
* not yours. Ownership is the lookup scope, so somebody else's collection
|
|
929
|
+
* and a non-existent one are indistinguishable.
|
|
930
|
+
*/
|
|
931
|
+
get(id: Id, options?: RequestOptions): Promise<MovieCollection>;
|
|
932
|
+
/**
|
|
933
|
+
* `POST /movie_collections` - a new manual list. `201`.
|
|
934
|
+
*
|
|
935
|
+
* `name` is the only field that survives: `before_create` overwrites `user`,
|
|
936
|
+
* pins `kind` to `"manual"` and sets `position` to `max(position) + 1`.
|
|
937
|
+
* Passing `kind: "favorites"` does not fail, it is just ignored, which is the
|
|
938
|
+
* point - there is exactly one system collection and only the listing may
|
|
939
|
+
* mint it.
|
|
940
|
+
*
|
|
941
|
+
* Names are not unique.
|
|
942
|
+
*
|
|
943
|
+
* @throws {OmsApiError} 400 `"Name can't be blank"`.
|
|
944
|
+
*/
|
|
945
|
+
create(input: CreateMovieCollectionInput | string, options?: RequestOptions): Promise<MovieCollection>;
|
|
946
|
+
/**
|
|
947
|
+
* `PATCH /movie_collections/:id` - renames or repositions. `200`.
|
|
948
|
+
*
|
|
949
|
+
* A real partial update: `update_params :name, :position` permits those two
|
|
950
|
+
* and nothing else, and an omitted key is left alone.
|
|
951
|
+
*
|
|
952
|
+
* Refused for the favourites row with
|
|
953
|
+
* `401 "You are not authorized to update this resource"` - the GENERIC
|
|
954
|
+
* message, because `updatable_by?` already returns false for a system
|
|
955
|
+
* collection and the friendlier "The favourites collection cannot be renamed,
|
|
956
|
+
* reordered or deleted" in `before_update` is never reached. Only
|
|
957
|
+
* {@link reorder} produces that sentence. Test with
|
|
958
|
+
* {@link isSystemMovieCollection} and hide the control instead.
|
|
959
|
+
*
|
|
960
|
+
* @throws {OmsApiError} 404 for a collection that is not yours, 401 for the
|
|
961
|
+
* favourites row.
|
|
962
|
+
*/
|
|
963
|
+
update(id: Id, input: UpdateMovieCollectionInput, options?: RequestOptions): Promise<MovieCollection>;
|
|
964
|
+
/**
|
|
965
|
+
* `DELETE /movie_collections/:id` - `204`, empty body.
|
|
966
|
+
*
|
|
967
|
+
* Cascades: `movie_collection_items` is `dependent: :destroy`, so every title
|
|
968
|
+
* in the list goes with it. Nothing is recoverable and nothing is asked.
|
|
969
|
+
*
|
|
970
|
+
* Refused for the favourites row with the same generic `401` as
|
|
971
|
+
* {@link update}, for the same reason.
|
|
972
|
+
*/
|
|
973
|
+
delete(id: Id, options?: RequestOptions): Promise<void>;
|
|
974
|
+
/**
|
|
975
|
+
* `POST /movie_collections/:id/reorder` - rewrites the order of the items
|
|
976
|
+
* inside a collection. Answers `200` with the COLLECTION, not the items.
|
|
977
|
+
*
|
|
978
|
+
* Send the full ordered list of item ids. The server keeps a stale client
|
|
979
|
+
* from losing rows: ids it recognises are laid out first in the order given,
|
|
980
|
+
* then every item you did NOT mention is appended in its existing relative
|
|
981
|
+
* order, and the whole sequence is renumbered densely from `0`. So an item
|
|
982
|
+
* added by another tab between your read and your write sinks to the bottom
|
|
983
|
+
* instead of vanishing. Ids that are not in this collection are ignored, not
|
|
984
|
+
* rejected.
|
|
985
|
+
*
|
|
986
|
+
* Positions are written with `update_column` inside one transaction, which
|
|
987
|
+
* skips validations and callbacks and - the part that catches people -
|
|
988
|
+
* does NOT touch each item's `updated_at`. A client that syncs on
|
|
989
|
+
* `updated_at` will not see a reorder. Refetch by position.
|
|
990
|
+
*
|
|
991
|
+
* The whole rewrite is one transaction, and items already at the right
|
|
992
|
+
* position are skipped, so a reorder that changes nothing costs no writes.
|
|
993
|
+
*
|
|
994
|
+
* Refused for the favourites row with
|
|
995
|
+
* `401 "The favourites collection cannot be renamed, reordered or deleted"` -
|
|
996
|
+
* this is the one action that produces that message rather than the generic
|
|
997
|
+
* one, because `reorder` calls `refuse_if_system!` itself before any
|
|
998
|
+
* authorisation runs.
|
|
999
|
+
*
|
|
1000
|
+
* @throws {OmsApiError} 404 for a collection that is not yours, 401 for the
|
|
1001
|
+
* favourites row.
|
|
1002
|
+
*/
|
|
1003
|
+
reorder(id: Id, itemIds: readonly Id[], options?: RequestOptions): Promise<MovieCollection>;
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* The `movies.watchProgress` namespace: how far into each title the user got,
|
|
1007
|
+
* and what "Continuar a ver" is built from.
|
|
1008
|
+
*
|
|
1009
|
+
* This controller is the odd one out. It overrides `index`, `create` and
|
|
1010
|
+
* `destroy` instead of inheriting `CrudActions`, so none of the list DSL
|
|
1011
|
+
* applies, `create` answers `200`, and there is no `show` and no `update`.
|
|
1012
|
+
* Everything is an upsert keyed on `(user, movie_id, video_id)`.
|
|
1013
|
+
*/
|
|
1014
|
+
export declare class MovieWatchProgressesNamespace extends Resource {
|
|
1015
|
+
/**
|
|
1016
|
+
* `GET /movie_watch_progresses` - the caller's rows, newest first.
|
|
1017
|
+
*
|
|
1018
|
+
* Not paginated, and not filterable. The controller ignores the query string
|
|
1019
|
+
* entirely and runs a fixed
|
|
1020
|
+
* `order(last_watched_at: :desc).limit(500)`, so:
|
|
1021
|
+
*
|
|
1022
|
+
* - there is NO way to reach row 501. A user with more history than that
|
|
1023
|
+
* simply cannot read the tail through this API;
|
|
1024
|
+
* - `search[...]` / `exact_search[...]` / `modifiers[...]` are not rejected,
|
|
1025
|
+
* they are silently ignored - this index never touches the code that
|
|
1026
|
+
* raises `400 "Unknown search filter"`. A client that thinks it asked for
|
|
1027
|
+
* one title gets all 500 rows and, if it trusts the filter, the wrong
|
|
1028
|
+
* answer. Filter client-side; that is why this method takes no params;
|
|
1029
|
+
* - there is no `ETag` either, because it does not go through
|
|
1030
|
+
* `resources_stale?`.
|
|
1031
|
+
*
|
|
1032
|
+
* Finished rows are included. Build "Continuar a ver" by dropping
|
|
1033
|
+
* `finished === true` yourself, and remember an episode can be finished while
|
|
1034
|
+
* its series is not.
|
|
1035
|
+
*
|
|
1036
|
+
* @throws {OmsApiError} 401 for an anonymous caller, 403 for an OAuth token.
|
|
1037
|
+
*/
|
|
1038
|
+
list(options?: RequestOptions): Promise<MovieWatchProgress[]>;
|
|
1039
|
+
/**
|
|
1040
|
+
* `POST /movie_watch_progresses` - upserts ONE row. **`200`, not `201`.**
|
|
1041
|
+
*
|
|
1042
|
+
* This is the playback tick: call it while something is playing, throttled
|
|
1043
|
+
* by the player to whatever interval you like. `MovieWatchProgress.upsert_for`
|
|
1044
|
+
* finds by `(user, movie_id, video_id)` and updates in place, so it is
|
|
1045
|
+
* idempotent and safe to repeat - the only create in this file where opting
|
|
1046
|
+
* into `options.retry` is a good idea rather than a way to make duplicates.
|
|
1047
|
+
* (The transport does not replay non-safe methods unless you ask.)
|
|
1048
|
+
*
|
|
1049
|
+
* `movie_type` is NOT part of the key. Posting the same `(movie_id,
|
|
1050
|
+
* video_id)` with a different type rewrites the existing row's type rather
|
|
1051
|
+
* than creating a second one.
|
|
1052
|
+
*
|
|
1053
|
+
* Read {@link MovieWatchProgressInput.finished} and
|
|
1054
|
+
* {@link MovieWatchProgressInput.last_watched_at} before using this: a tick
|
|
1055
|
+
* that omits `last_watched_at` does not move the title up the list, and
|
|
1056
|
+
* `finished` has three states rather than two. A `finished` of `null` is
|
|
1057
|
+
* dropped by this method rather than sent, matching what the server would do
|
|
1058
|
+
* with it, so `finished` reaches the wire only as a real boolean.
|
|
1059
|
+
*
|
|
1060
|
+
* Use {@link saveMany} instead when you have more than a couple of rows -
|
|
1061
|
+
* see its docs for why one request is not just faster but differently shaped.
|
|
1062
|
+
*
|
|
1063
|
+
* @throws {OmsError} `invalid_request` when `movie_id`, `video_id` or
|
|
1064
|
+
* `movie_type` is blank.
|
|
1065
|
+
* @throws {OmsApiError} 400 `"movie_id, video_id, movie_type are required"`
|
|
1066
|
+
* from the server's own check, or a validation sentence such as
|
|
1067
|
+
* `"Position must be greater than or equal to 0"`.
|
|
1068
|
+
*/
|
|
1069
|
+
save(input: MovieWatchProgressInput, options?: RequestOptions): Promise<MovieWatchProgress>;
|
|
1070
|
+
/**
|
|
1071
|
+
* `POST /movie_watch_progresses/bulk` - upserts up to
|
|
1072
|
+
* {@link MOVIE_WATCH_BULK_LIMIT} rows in ONE request and ONE transaction.
|
|
1073
|
+
* `200`, with the saved rows in the order sent.
|
|
1074
|
+
*
|
|
1075
|
+
* ## Why this endpoint exists, and when to reach for it
|
|
1076
|
+
*
|
|
1077
|
+
* "Marcar temporada como vista" is one row per episode. A 24-episode season
|
|
1078
|
+
* through {@link save} is 24 POSTs: 24 round trips, 24 Puma threads taken in
|
|
1079
|
+
* turn, 24 chances for one to fail and leave the season half-marked, and 24
|
|
1080
|
+
* requests against the caller's 600/min ceiling. The comment on
|
|
1081
|
+
* `BULK_LIMIT` says it outright - "Marking a whole season watched would
|
|
1082
|
+
* otherwise be one POST per episode". This collapses it into one.
|
|
1083
|
+
*
|
|
1084
|
+
* The transaction is the other half of the point, and it cuts both ways:
|
|
1085
|
+
*
|
|
1086
|
+
* - **use {@link saveMany}** for a statement about several rows at once that
|
|
1087
|
+
* must be all-or-nothing - mark a season watched or unwatched, restore a
|
|
1088
|
+
* device's offline queue, seed history on first sync. If any entry is
|
|
1089
|
+
* invalid the whole batch rolls back and answers `400`, so you never end up
|
|
1090
|
+
* with episodes 1-9 marked and 10-24 not.
|
|
1091
|
+
* - **use {@link save}** for the continuous playback tick. One row, and a
|
|
1092
|
+
* failure costs one tick that the next one will overwrite anyway. Batching
|
|
1093
|
+
* ticks would trade a lost second for a lost minute.
|
|
1094
|
+
*
|
|
1095
|
+
* ## The silent-truncation trap this method closes
|
|
1096
|
+
*
|
|
1097
|
+
* The server does `Array(params[:items]).first(200)`: entries past the limit
|
|
1098
|
+
* are dropped without a word and the response is a cheerful `200` listing the
|
|
1099
|
+
* 200 that were saved. A 300-episode batch would look like it worked. This
|
|
1100
|
+
* method raises before sending instead, so split the work yourself - and note
|
|
1101
|
+
* that separate batches are separate transactions, so a split is no longer
|
|
1102
|
+
* atomic end to end.
|
|
1103
|
+
*
|
|
1104
|
+
* Retrying the whole batch is safe: every entry is the same upsert
|
|
1105
|
+
* {@link save} performs.
|
|
1106
|
+
*
|
|
1107
|
+
* @throws {OmsError} `invalid_request` for an empty list, for more than
|
|
1108
|
+
* {@link MOVIE_WATCH_BULK_LIMIT} entries, or for an entry missing
|
|
1109
|
+
* `movie_id`, `video_id` or `movie_type`.
|
|
1110
|
+
* @throws {OmsApiError} 400 `"items is required"` for an empty list that got
|
|
1111
|
+
* through, or the first failing entry's validation sentence - and in that
|
|
1112
|
+
* case NOTHING was saved.
|
|
1113
|
+
*/
|
|
1114
|
+
saveMany(inputs: readonly MovieWatchProgressInput[], options?: RequestOptions): Promise<MovieWatchProgress[]>;
|
|
1115
|
+
/**
|
|
1116
|
+
* Marks one playable watched or unwatched, explicitly.
|
|
1117
|
+
*
|
|
1118
|
+
* Sugar over {@link save} that exists because this is the exact call the
|
|
1119
|
+
* fixed bug was about. Marking something UNWATCHED sends `position: 0,
|
|
1120
|
+
* duration: 0`, and with `finished` absent the model's `set_finished` bails
|
|
1121
|
+
* on its `duration <= 0` guard and leaves the stored flag as it was - so the
|
|
1122
|
+
* tick never came off. Passing the boolean outright sets `finished_given`,
|
|
1123
|
+
* which makes the model skip that guard and write what you said.
|
|
1124
|
+
*
|
|
1125
|
+
* Defaults `position` and `duration` to `0` when you do not supply them,
|
|
1126
|
+
* which is right for "mark unwatched" and harmless for "mark watched"
|
|
1127
|
+
* precisely because the explicit flag stops the server deriving anything from
|
|
1128
|
+
* them. Supply the real numbers when you have them; the progress bar reads
|
|
1129
|
+
* them.
|
|
1130
|
+
*
|
|
1131
|
+
* `last_watched_at` is still yours to send, and still matters: marking an
|
|
1132
|
+
* episode watched without one leaves the series where it was in the list.
|
|
1133
|
+
*/
|
|
1134
|
+
setWatched(input: Omit<MovieWatchProgressInput, "finished">, watched: boolean, options?: RequestOptions): Promise<MovieWatchProgress>;
|
|
1135
|
+
/**
|
|
1136
|
+
* `DELETE /movie_watch_progresses/:id` - forgets ONE playable. `204`, empty
|
|
1137
|
+
* body.
|
|
1138
|
+
*
|
|
1139
|
+
* The id is the progress row's primary key, which for a series is one
|
|
1140
|
+
* episode. To forget a whole title use {@link forgetMovie}.
|
|
1141
|
+
*
|
|
1142
|
+
* @throws {OmsApiError} 404 `"Resource not found"` for a row that is not
|
|
1143
|
+
* yours.
|
|
1144
|
+
*/
|
|
1145
|
+
delete(id: Id, options?: RequestOptions): Promise<void>;
|
|
1146
|
+
/**
|
|
1147
|
+
* `DELETE /movie_watch_progresses/for_movie?movie_id=...` - forgets EVERY row
|
|
1148
|
+
* for a title. `204`, empty body.
|
|
1149
|
+
*
|
|
1150
|
+
* This is the "remover de Continuar a ver" button. For a series it destroys
|
|
1151
|
+
* the progress of every episode, not just the one on screen; there is no
|
|
1152
|
+
* per-season form and no undo.
|
|
1153
|
+
*
|
|
1154
|
+
* Note the verb: despite the `?movie_id=` query string this is a **DELETE**,
|
|
1155
|
+
* not a read. It is a collection route, so the `movie_id` is the addon's id
|
|
1156
|
+
* for the title (an IMDb id, say), never a `movie_watch_progresses` primary
|
|
1157
|
+
* key.
|
|
1158
|
+
*
|
|
1159
|
+
* `204` even when nothing matched, so the answer does not tell you whether
|
|
1160
|
+
* anything was there.
|
|
1161
|
+
*
|
|
1162
|
+
* @throws {OmsError} `invalid_request` for a blank id.
|
|
1163
|
+
* @throws {OmsApiError} 400 `"movie_id is required"`.
|
|
1164
|
+
*/
|
|
1165
|
+
forgetMovie(movieId: string, options?: RequestOptions): Promise<void>;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* The `movies` namespace, reachable as `oms.movies`.
|
|
1169
|
+
*
|
|
1170
|
+
* Everything under it needs a session or a personal token. An OAuth access
|
|
1171
|
+
* token cannot reach any of it: `enforce_oauth_scope!` denies by omission and
|
|
1172
|
+
* no movies controller declares an `oauth_scope`, so a third-party client gets
|
|
1173
|
+
* `403 {"error":"insufficient_scope", "message": "This endpoint is not
|
|
1174
|
+
* reachable with an OAuth access token..."}` - one of the few structured error
|
|
1175
|
+
* bodies the API emits. That is a deliberate gate, not an oversight to route
|
|
1176
|
+
* around.
|
|
1177
|
+
*/
|
|
1178
|
+
export declare class MoviesNamespace extends Resource {
|
|
1179
|
+
/** Installed Stremio addons, plus `.groups` and `.grants` for sharing them. */
|
|
1180
|
+
readonly addons: MovieAddonsNamespace;
|
|
1181
|
+
/** Favourites and hand-made lists, plus `.items` for their contents. */
|
|
1182
|
+
readonly collections: MovieCollectionsNamespace;
|
|
1183
|
+
/** Playback position per title or episode; "Continuar a ver". */
|
|
1184
|
+
readonly watchProgress: MovieWatchProgressesNamespace;
|
|
1185
|
+
constructor(http: ApiClient);
|
|
1186
|
+
}
|