Skip to content

Commit 601dfe5

Browse files
committed
feat: implement tvdb auth
1 parent dce4628 commit 601dfe5

11 files changed

+160
-86
lines changed

src/models/tvdb/tvdb-client.model.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ import {
1414
* @see [documentation]{@link https://thetvdb.github.io/v4-api/#/Login/post_login}
1515
*/
1616
export type TvdbClientSettings = {
17-
/** The domain name (i.e. https://api4.thetvdb.com/v4) */
17+
/** The domain name (e.g. https://api4.thetvdb.com) */
1818
endpoint: string;
19+
/** The api version (e.g. v4) */
20+
version: string;
1921
/** The consumer client identifier */
2022
useragent: string;
21-
/** The app secret */
23+
/** The app api key */
2224
apiKey: string;
2325
/** token time-to-live (28 days) */
2426
tokenTTL: number;
@@ -27,8 +29,8 @@ export type TvdbClientSettings = {
2729
export type TvdbClientOptions = BaseOptions<TvdbClientSettings, TvdbApiResponse>;
2830

2931
export type TvdbClientAuthentication = {
30-
access_token?: string;
31-
user_pin?: string;
32+
accessToken?: string;
33+
userPin?: string;
3234
expires?: number;
3335
};
3436

src/models/tvdb/tvdb-search.model.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export type TvdbSearchQuery = {
5151
};
5252

5353
export type TvdbSearch = {
54+
objectID: string;
5455
aliases: string[];
5556
companies: string[];
5657
companyType: string;
@@ -64,7 +65,6 @@ export type TvdbSearch = {
6465
is_official: boolean;
6566
name_translated: string;
6667
network: string;
67-
objectID: string;
6868
officialList: string;
6969
overview: string;
7070
/** Record of languages with their translations */

src/services/common/base-client.ts

+32-34
Original file line numberDiff line numberDiff line change
@@ -122,20 +122,13 @@ export abstract class BaseClient<
122122
SettingsType extends RecursiveRecord = RecursiveRecord,
123123
AuthenticationType extends RecursiveRecord = RecursiveRecord,
124124
> {
125-
protected _cache: CacheStore<ResponseType>;
126-
protected _settings: SettingsType;
127-
protected _authentication: ObservableState<AuthenticationType>;
128-
protected _callListeners: Observable<QueryType>;
125+
private readonly _settings: SettingsType;
126+
private _cache: CacheStore<ResponseType>;
127+
private _authentication: ObservableState<AuthenticationType>;
128+
private _callListeners: Observable<QueryType>;
129129

130-
/**
131-
* Clears the cache entry for the specified key.
132-
* If no key is provided, clears the entire cache.
133-
*
134-
* @param key - The cache key.
135-
*/
136-
clearCache(key?: string) {
137-
if (key) return this._cache?.delete(key);
138-
return this._cache?.clear();
130+
protected get settings() {
131+
return this._settings;
139132
}
140133

141134
/**
@@ -192,6 +185,17 @@ export abstract class BaseClient<
192185
};
193186
}
194187

188+
/**
189+
* Clears the cache entry for the specified key.
190+
* If no key is provided, clears the entire cache.
191+
*
192+
* @param key - The cache key.
193+
*/
194+
clearCache(key?: string) {
195+
if (key) return this._cache?.delete(key);
196+
return this._cache?.clear();
197+
}
198+
195199
/**
196200
* Binds BaseTraktClient _call instance to the endpoint instance and the call method of the endpoint
197201
*
@@ -421,26 +425,6 @@ export const parseUrl = <P extends RecursiveRecord = RecursiveRecord, O extends
421425
const [pathPart, queryPart] = template.url.split('?');
422426

423427
let path = pathPart;
424-
const queryParams: URLSearchParams = new URLSearchParams(queryPart);
425-
426-
if (queryPart) {
427-
queryParams.forEach((value, key, parent) => {
428-
const _value = params[key] ?? value;
429-
430-
// If a value is found we encode
431-
if (_value !== undefined && _value !== '') {
432-
queryParams.set(key, typeof _value === 'object' ? JSON.stringify(_value) : _value);
433-
}
434-
// If the parameter is required we raise error
435-
else if (template.opts?.parameters?.query?.[key] === true) {
436-
throw Error(`Missing mandatory query parameter: '${key}'`);
437-
}
438-
// else we remove the empty field from parameters
439-
else {
440-
parent.delete(key);
441-
}
442-
});
443-
}
444428

445429
// fill query path parameter i.e :variable
446430
if (pathPart.includes(':')) {
@@ -462,6 +446,20 @@ export const parseUrl = <P extends RecursiveRecord = RecursiveRecord, O extends
462446
}
463447

464448
const url = new URL(path, endpoint);
465-
url.search = queryParams.toString();
449+
450+
if (queryPart) {
451+
new URLSearchParams(queryPart).forEach((value, key) => {
452+
const _value = params[key] ?? value;
453+
454+
// If a value is found we encode
455+
if (_value !== undefined && _value !== '') {
456+
url.searchParams.set(key, typeof _value === 'object' ? JSON.stringify(_value) : _value);
457+
}
458+
// If the parameter is required we raise error
459+
else if (template.opts?.parameters?.query?.[key] === true) {
460+
throw Error(`Missing mandatory query parameter: '${key}'`);
461+
}
462+
});
463+
}
466464
return url;
467465
};

src/services/trakt-client/clients/base-trakt-client.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,10 @@ export class BaseTraktClient
144144
*/
145145
protected _parseHeaders<T extends TraktApiParams = TraktApiParams>(template: TraktApiTemplate<T>): HeadersInit {
146146
const headers: HeadersInit = {
147-
[TraktApiHeaders.UserAgent]: this._settings.useragent,
147+
[TraktApiHeaders.UserAgent]: this.settings.useragent,
148148
[TraktApiHeaders.ContentType]: BaseHeaderContentType.Json,
149149
[TraktApiHeaders.TraktApiVersion]: '2',
150-
[TraktApiHeaders.TraktApiKey]: this._settings.client_id,
150+
[TraktApiHeaders.TraktApiKey]: this.settings.client_id,
151151
};
152152

153153
if (template.opts?.auth === true && !this.auth.access_token) {
@@ -174,7 +174,7 @@ export class BaseTraktClient
174174
* @throws {Error} Throws an error if mandatory parameters are missing or if a filter is not supported.
175175
*/
176176
protected _parseUrl<T extends TraktApiParams = TraktApiParams>(template: TraktApiTemplate<T>, params: T): URL {
177-
const url = parseUrl<T>(template, params, this._settings.endpoint);
177+
const url = parseUrl<T>(template, params, this.settings.endpoint);
178178
const queryParams = url.searchParams;
179179

180180
// Adds Filters query parameters

src/services/trakt-client/clients/trakt-client.ts

+28-33
Original file line numberDiff line numberDiff line change
@@ -52,22 +52,21 @@ export class TraktClient extends BaseTraktClient {
5252
/**
5353
* Exchanges an authorization code or refresh token for an access token.
5454
*
55-
* @private
56-
*
5755
* @param request - The request object containing the code or refresh token.
5856
*
5957
* @returns A promise resolving to the updated Trakt authentication information.
6058
*
6159
* @throws Error Throws an error if the exchange fails or an error is received from the server.
6260
*
61+
* @private
62+
*
6363
* @see handleError
64-
* @memberof TraktClient
6564
*/
6665
private async _exchange(request: Pick<TraktAuthenticationCodeRequest, 'code'> | Pick<TraktAuthenticationRefreshRequest, 'refresh_token'>) {
6766
const _request: TraktAuthenticationBaseRequest = {
68-
client_id: this._settings.client_id,
69-
client_secret: this._settings.client_secret,
70-
redirect_uri: this._settings.redirect_uri,
67+
client_id: this.settings.client_id,
68+
client_secret: this.settings.client_secret,
69+
redirect_uri: this.settings.redirect_uri,
7170
grant_type: 'code' in request ? 'authorization_code' : 'refresh_token',
7271
...request,
7372
};
@@ -98,24 +97,23 @@ export class TraktClient extends BaseTraktClient {
9897
/**
9998
* Revokes the current authentication by invalidating the access token.
10099
*
101-
* @private
102-
*
103100
* @paramrequest - Additional parameters for revoking authentication.
104101
*
105102
* @returns A promise resolving when the authentication is successfully revoked.
106103
*
107104
* @throws Error Throws an error if no access token is found.
108105
*
106+
* @private
107+
*
109108
* @see isResponseOk
110-
* @memberof TraktClient
111109
*/
112110
private async _revoke(request: Partial<TraktAuthenticationRevokeRequest> = {}) {
113111
if (!request && !this.auth.access_token) throw new Error('No access token found.');
114112

115113
const _request: TraktAuthenticationRevokeRequest = {
116114
token: this.auth.access_token!,
117-
client_id: this._settings.client_id,
118-
client_secret: this._settings.client_secret,
115+
client_id: this.settings.client_id,
116+
client_secret: this.settings.client_secret,
119117
...request,
120118
};
121119

@@ -129,8 +127,6 @@ export class TraktClient extends BaseTraktClient {
129127
/**
130128
* Initiates device authentication and retrieves the device code.
131129
*
132-
* @private
133-
*
134130
* @template T - The type of the authentication information to be returned (string means auth token, null means codes).
135131
*
136132
* @param {T extends string | null} code - The device code (if polling) or null to initiate a new device authentication.
@@ -139,21 +135,22 @@ export class TraktClient extends BaseTraktClient {
139135
*
140136
* @throws Error Throws an error if the device authentication fails.
141137
*
138+
* @private
139+
*
142140
* @see handleError
143-
* @memberof TraktClient
144141
*/
145142
private async _device<T extends string | null>(code: T): Promise<T extends null ? TraktDeviceAuthentication : TraktAuthentication> {
146143
try {
147144
let response: TraktApiResponse<TraktAuthentication | TraktDeviceAuthentication>;
148145
if (code) {
149146
response = await this.authentication.device.token({
150-
client_id: this._settings.client_id,
151-
client_secret: this._settings.client_secret,
147+
client_id: this.settings.client_id,
148+
client_secret: this.settings.client_secret,
152149
code,
153150
});
154151
} else {
155152
response = await this.authentication.device.code({
156-
client_id: this._settings.client_id,
153+
client_id: this.settings.client_id,
157154
});
158155
}
159156
return (await response.json()) as T extends null ? TraktDeviceAuthentication : TraktAuthentication;
@@ -162,6 +159,18 @@ export class TraktClient extends BaseTraktClient {
162159
}
163160
}
164161

162+
/**
163+
* Polls the device authentication endpoint to complete the authentication.
164+
* If the timeout is reached, the polling is cancelled and an error is thrown.
165+
* If the authentication is successful, the polling is cancelled and the authentication information is returned.
166+
*
167+
* @param poll - The device authentication information.
168+
* @param timeout - The timeout in milliseconds.
169+
*
170+
* @returns A promise resolving to the authentication information if successful
171+
*
172+
* @private
173+
*/
165174
private async _devicePolling(poll: TraktDeviceAuthentication, timeout: number) {
166175
if (timeout <= Date.now()) {
167176
clearInterval(this.polling);
@@ -198,8 +207,6 @@ export class TraktClient extends BaseTraktClient {
198207
* The code should then be used in conjunction with the {@link pollWithDeviceCode} method to finish authentication.
199208
*
200209
* @returns A promise resolving to the device authentication information.
201-
*
202-
* @memberof TraktClient
203210
*/
204211
getDeviceCode() {
205212
return this._device(null);
@@ -211,8 +218,6 @@ export class TraktClient extends BaseTraktClient {
211218
* @param poll - The device authentication information.
212219
*
213220
* @returns A promise resolving to the completed authentication information or `undefined`.
214-
*
215-
* @memberof TraktClient
216221
*/
217222
pollWithDeviceCode(poll: TraktDeviceAuthentication) {
218223
if (this.polling) {
@@ -244,8 +249,6 @@ export class TraktClient extends BaseTraktClient {
244249
* @returns A promise resolving to the response from the Trakt website.
245250
*
246251
* @see [authorize]{@link https://trakt.docs.apiary.io/#reference/authentication-oauth/authorize}
247-
*
248-
* @memberof TraktClient
249252
*/
250253
redirectToAuthentication(request: Pick<TraktAuthenticationAuthorizeRequest, 'state' | 'signup' | 'prompt'> & { redirect?: RequestRedirect } = {}) {
251254
this.updateAuth(auth => ({ ...auth, state: request.state ?? randomHex() }));
@@ -257,8 +260,8 @@ export class TraktClient extends BaseTraktClient {
257260
return this.authentication.oAuth.authorize(
258261
{
259262
response_type: 'code',
260-
client_id: this._settings.client_id,
261-
redirect_uri: this._settings.redirect_uri,
263+
client_id: this.settings.client_id,
264+
redirect_uri: this.settings.redirect_uri,
262265
state: this.auth.state,
263266
...request,
264267
},
@@ -275,8 +278,6 @@ export class TraktClient extends BaseTraktClient {
275278
* @returns A promise resolving to the Trakt authentication information.
276279
*
277280
* @throws Error Throws an error if the CSRF token is invalid.
278-
*
279-
* @memberof TraktClient
280281
*/
281282
exchangeCodeForToken(code: string, state?: string) {
282283
if (state && state !== this.auth.state) throw Error('Invalid CSRF (State)');
@@ -289,8 +290,6 @@ export class TraktClient extends BaseTraktClient {
289290
* @returns A promise resolving to the updated Trakt authentication information.
290291
*
291292
* @throws Error Throws an error if no refresh token is found.
292-
*
293-
* @memberof TraktClient
294293
*/
295294
refreshToken() {
296295
if (!this.auth.refresh_token) {
@@ -305,8 +304,6 @@ export class TraktClient extends BaseTraktClient {
305304
* @returns A promise resolving when the authentication is successfully revoked.
306305
*
307306
* @throws Error Throws an error if no access token is found.
308-
*
309-
* @memberof TraktClient
310307
*/
311308
async revokeAuthentication(): Promise<void> {
312309
if (this.auth.access_token) {
@@ -322,8 +319,6 @@ export class TraktClient extends BaseTraktClient {
322319
* @param auth - The Trakt authentication information to import.
323320
*
324321
* @returns A promise resolving to the imported Trakt authentication information.
325-
*
326-
* @memberof TraktClient
327322
*/
328323
async importAuthentication(auth: TraktClientAuthentication): Promise<TraktClientAuthentication> {
329324
this.updateAuth(auth);

0 commit comments

Comments
 (0)