-
-
Notifications
You must be signed in to change notification settings - Fork 838
feat(watchlist): implement Plex watchlist GraphQL API support #3008
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
1c6a8e7
315ed09
480ffe9
27cbe73
90c712d
fa801fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -98,15 +98,37 @@ | |
| }; | ||
| } | ||
|
|
||
| interface WatchlistResponse { | ||
| interface HomeUsersResponse { | ||
| MediaContainer: { | ||
| totalSize: number; | ||
| Metadata?: { | ||
| ratingKey: string; | ||
| User: { | ||
| $: { | ||
| id: string; | ||
| uuid: string; | ||
| title: string; | ||
| username: string; | ||
| email: string; | ||
| thumb: string; | ||
| }; | ||
| }[]; | ||
| }; | ||
| } | ||
|
|
||
| interface WatchlistResponse { | ||
| data?: { | ||
| user: { | ||
| watchlist: { | ||
| nodes: { | ||
| id: string; | ||
| }[]; | ||
| pageInfo: { | ||
| hasNextPage: boolean; | ||
| endCursor: string; | ||
| }; | ||
| }; | ||
| }; | ||
| } | null; | ||
| } | ||
|
|
||
| type PlexMetadataItem = { | ||
| ratingKey: string; | ||
| type: 'movie' | 'show'; | ||
|
|
@@ -115,6 +137,7 @@ | |
| id: `imdb://tt${number}` | `tmdb://${number}` | `tvdb://${number}`; | ||
| }[]; | ||
| }; | ||
|
|
||
| interface MetadataResponse { | ||
| MediaContainer: { | ||
| Metadata?: PlexMetadataItem[]; | ||
|
|
@@ -268,35 +291,65 @@ | |
| return parsedXml; | ||
| } | ||
|
|
||
| public async getWatchlist({ | ||
| offset = 0, | ||
| size = 20, | ||
| }: { offset?: number; size?: number } = {}): Promise<{ | ||
| offset: number; | ||
| size: number; | ||
| public async getHomeUsers(): Promise<HomeUsersResponse> { | ||
| const response = await this.axios.get('/api/home/users', { | ||
| transformResponse: [], | ||
| responseType: 'text', | ||
| }); | ||
Check failureCode scanning / CodeQL Server-side request forgery Critical
The
host Error loading related location Loading user-provided value Error loading related location Loading The host Error loading related location Loading user-provided value Error loading related location Loading |
||
|
|
||
| const parsedXml = (await xml2js.parseStringPromise( | ||
| response.data | ||
| )) as HomeUsersResponse; | ||
| return parsedXml; | ||
| } | ||
|
|
||
| public async getWatchlist( | ||
| uuid: string, | ||
| { | ||
| first = 20, | ||
| after = undefined, | ||
| }: { first?: number; after?: string | undefined } = {} | ||
| ): Promise<{ | ||
| first: number; | ||
| after: string | undefined; | ||
| endCursor: string | undefined; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| totalSize: number; | ||
| items: PlexWatchlistItem[]; | ||
|
Comment on lines
+306
to
317
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This only processes the first watchlist page.
Also applies to: 323-352, 364-433 🤖 Prompt for AI Agents
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Known issue and to be done as stated in the PR description. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
(。•ᴗ•。)🐇 |
||
| }> { | ||
| try { | ||
| const watchlistCache = cacheManager.getCache('plexwatchlist'); | ||
| let cachedWatchlist = watchlistCache.data.get<PlexWatchlistCache>( | ||
| this.authToken | ||
| ); | ||
| let cachedWatchlist = watchlistCache.data.get<PlexWatchlistCache>(uuid); | ||
|
|
||
| const response = await this.axios.get<WatchlistResponse>( | ||
| '/library/sections/watchlist/all', | ||
| const response = await this.axios.post<WatchlistResponse>( | ||
| '/api', | ||
| { | ||
| params: { | ||
| 'X-Plex-Container-Start': offset, | ||
| 'X-Plex-Container-Size': size, | ||
| }, | ||
| headers: { | ||
| 'If-None-Match': cachedWatchlist?.etag, | ||
| query: `query GetWatchlistHub($uuid: ID = "", $first: PaginationInt!, $after: String) { | ||
| user(id: $uuid) { | ||
| watchlist(first: $first, after: $after) { | ||
| nodes { | ||
| ...itemFields | ||
| } | ||
| pageInfo { | ||
| hasNextPage | ||
| endCursor | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fragment itemFields on MetadataItem { | ||
| id | ||
| }`, | ||
| variables: { | ||
| uuid: uuid, | ||
| first: first, | ||
| after: after, | ||
| }, | ||
| baseURL: 'https://discover.provider.plex.tv', | ||
| validateStatus: (status) => status < 400, // Allow HTTP 304 to return without error | ||
| }, | ||
| { | ||
| baseURL: 'https://community.plex.tv', | ||
| } | ||
| ); | ||
Check failureCode scanning / CodeQL Server-side request forgery Critical
The
host Error loading related location Loading user-provided value Error loading related location Loading The host Error loading related location Loading user-provided value Error loading related location Loading |
||
|
|
||
| // If we don't recieve HTTP 304, the watchlist has been updated and we need to update the cache. | ||
| if (response.status >= 200 && response.status <= 299) { | ||
|
|
@@ -305,27 +358,24 @@ | |
| response: response.data, | ||
| }; | ||
|
|
||
| watchlistCache.data.set<PlexWatchlistCache>( | ||
| this.authToken, | ||
| cachedWatchlist | ||
| ); | ||
| watchlistCache.data.set<PlexWatchlistCache>(uuid, cachedWatchlist); | ||
| } | ||
|
|
||
| const watchlistDetails = await Promise.all( | ||
| (cachedWatchlist?.response.MediaContainer.Metadata ?? []).map( | ||
| (cachedWatchlist?.response.data?.user.watchlist.nodes ?? []).map( | ||
| async (watchlistItem) => { | ||
| let detailedResponse: MetadataResponse; | ||
| try { | ||
| detailedResponse = await this.getRolling<MetadataResponse>( | ||
| `/library/metadata/${watchlistItem.ratingKey}`, | ||
| `/library/metadata/${watchlistItem.id}`, | ||
| { | ||
| baseURL: 'https://discover.provider.plex.tv', | ||
| } | ||
| ); | ||
| } catch (e) { | ||
| if (e.response?.status === 404) { | ||
| logger.warn( | ||
| `Item with ratingKey ${watchlistItem.ratingKey} not found, it may have been removed from the server.`, | ||
| `Item with id ${watchlistItem.id} not found, it may have been removed from the server.`, | ||
| { label: 'Plex.TV Metadata API' } | ||
| ); | ||
| return null; | ||
|
|
@@ -340,7 +390,7 @@ | |
|
|
||
| if (!metadata) { | ||
| logger.warn( | ||
| `Item with ratingKey ${watchlistItem.ratingKey} returned no metadata, skipping.`, | ||
| `Item with id ${watchlistItem.id} returned no metadata, skipping.`, | ||
| { label: 'Plex.TV Metadata API' } | ||
| ); | ||
| return null; | ||
|
|
@@ -373,9 +423,12 @@ | |
| ) as PlexWatchlistItem[]; | ||
|
|
||
| return { | ||
| offset, | ||
| size, | ||
| totalSize: cachedWatchlist?.response.MediaContainer.totalSize ?? 0, | ||
| first, | ||
| after, | ||
| endCursor: | ||
| cachedWatchlist?.response.data?.user.watchlist.pageInfo.endCursor, | ||
| totalSize: | ||
| cachedWatchlist?.response.data?.user.watchlist.nodes.length ?? 0, | ||
| items: filteredList, | ||
| }; | ||
| } catch (e) { | ||
|
|
@@ -384,8 +437,9 @@ | |
| errorMessage: e.message, | ||
| }); | ||
| return { | ||
| offset, | ||
| size, | ||
| first, | ||
| after, | ||
| endCursor: undefined, | ||
| totalSize: 0, | ||
| items: [], | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import type { MigrationInterface, QueryRunner } from 'typeorm'; | ||
|
|
||
| export class AddPlexUuidToUser1777262170937 implements MigrationInterface { | ||
| name = 'AddPlexUuidToUser1777262170937'; | ||
|
|
||
| public async up(queryRunner: QueryRunner): Promise<void> { | ||
| await queryRunner.query( | ||
| `ALTER TABLE "user" ADD "plexUuid" character varying` | ||
| ); | ||
| } | ||
|
|
||
| public async down(queryRunner: QueryRunner): Promise<void> { | ||
| await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "plexUuid"`); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.