cleaned up Get All Libraries endpoint

This commit is contained in:
JasonLandbridge
2025-04-05 11:13:38 +02:00
parent 5cbcef13ba
commit 462b30f914
24 changed files with 426 additions and 190 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -30,6 +30,7 @@
}, },
"devDependencies": { "devDependencies": {
"@modyfi/vite-plugin-yaml": "^1.1.0", "@modyfi/vite-plugin-yaml": "^1.1.0",
"@ngneat/falso": "^7.3.0",
"@redocly/cli": "^1.28.1", "@redocly/cli": "^1.28.1",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^22.5.0", "@types/node": "^22.5.0",

View File

@@ -0,0 +1,8 @@
type: object
required:
- agent
properties:
agent:
type: string
description: The Plex agent used to match and retrieve media metadata.
example: tv.plex.agents.movie

View File

@@ -0,0 +1,8 @@
type: object
required:
- composite
properties:
composite:
type: string
description: The relative path to the composite media item.
example: /library/sections/1/composite/1743824484

View File

@@ -0,0 +1,9 @@
type: object
required:
- contentChangedAt
properties:
contentChangedAt:
type: integer
format: int32
description: The number of seconds since the content was last changed relative to now.
example: 9173960

View File

@@ -0,0 +1,8 @@
type: object
required:
- content
properties:
content:
type: boolean
description: UNKNOWN
example: true

View File

@@ -0,0 +1,8 @@
type: object
properties:
createdAt:
allOf:
- $ref: "../../models/common/PlexDateTime.yaml"
- description: "The date and time when the library was created."
type: integer
format: int64

View File

@@ -0,0 +1,8 @@
type: object
required:
- directory
properties:
directory:
type: boolean
description: UNKNOWN
example: true

View File

@@ -0,0 +1,8 @@
type: object
required:
- filters
properties:
filters:
type: boolean
description: UNKNOWN
example: true

View File

@@ -0,0 +1,10 @@
type: object
required:
- hidden
properties:
hidden:
allOf:
- $ref: "../common/PlexBoolean.yaml"
- type: integer
format: int32
description: UNKNOWN

View File

@@ -0,0 +1,8 @@
type: object
required:
- key
properties:
key:
type: string
description: The library key representing the unique identifier
example: "1"

View File

@@ -0,0 +1,8 @@
type: object
required:
- language
properties:
language:
type: string
description: The Plex library language that has been set
example: en-US

View File

@@ -0,0 +1,8 @@
type: object
required:
- refreshing
properties:
refreshing:
type: boolean
description: "Indicates whether the library is currently being refreshed or updated"
example: true

View File

@@ -0,0 +1,6 @@
type: object
required:
- scannedAt
properties:
scannedAt:
$ref: "../../models/common/PlexDateTime.yaml"

View File

@@ -0,0 +1,8 @@
type: object
required:
- scanner
properties:
scanner:
type: string
description: UNKNOWN
example: Plex Movie

View File

@@ -0,0 +1,8 @@
type: object
required:
- title
properties:
title:
type: string
description: "The title of the library"
example: "Movies"

View File

@@ -0,0 +1,9 @@
type: object
required:
- type
properties:
type:
allOf:
- $ref: "../common/PlexMediaTypeString.yaml"
- type: string
description: "The library type"

View File

@@ -0,0 +1,6 @@
type: object
required:
- updatedAt
properties:
updatedAt:
$ref: "../../models/common/PlexDateTime.yaml"

View File

@@ -0,0 +1,8 @@
type: object
required:
- uuid
properties:
uuid:
type: string
description: "The universally unique identifier for the library."
example: "e69655a2-ef48-4aba-bb19-01e7d3cc34d6"

View File

@@ -10,6 +10,8 @@ get:
Libraries have features beyond just being a collection of media; for starters, they include information about supported types, filters and sorts. Libraries have features beyond just being a collection of media; for starters, they include information about supported types, filters and sorts.
This allows a client to provide a rich interface around the media (e.g. allow sorting movies by release year). This allows a client to provide a rich interface around the media (e.g. allow sorting movies by release year).
parameters:
- $ref: "../../parameters/accept-application-json.yaml"
responses: responses:
"200": "200":
description: The libraries available on the Server description: The libraries available on the Server
@@ -17,126 +19,59 @@ get:
application/json: application/json:
schema: schema:
type: object type: object
required:
- MediaContainer
properties: properties:
MediaContainer: MediaContainer:
type: object allOf:
required: - $ref: "../../models/media-container/size.yaml"
- size - $ref: "../../models/media-container/allow-sync.yaml"
- allowSync - $ref: "../../models/media-container/title1.yaml"
- title1 - type: object
- Directory properties:
properties: Directory:
size: type: array
type: integer items:
format: int32 required:
example: 5 - Location
allowSync: allOf:
type: boolean - $ref: "../../models/media-container/allow-sync.yaml"
example: false - $ref: "../../models/media-container/art.yaml"
title1: - $ref: "../../models/directory/composite.yaml"
type: string - $ref: "../../models/directory/filters.yaml"
example: Plex Library - $ref: "../../models/directory/refreshing.yaml"
Directory: - $ref: "../../models/media-container/thumb.yaml"
type: array - $ref: "../../models/directory/key.yaml"
items: - $ref: "../../models/directory/type.yaml"
type: object - $ref: "../../models/directory/title.yaml"
required: - $ref: "../../models/directory/agent.yaml"
- allowSync - $ref: "../../models/directory/scanner.yaml"
- art - $ref: "../../models/directory/language.yaml"
- composite - $ref: "../../models/directory/uuid.yaml"
- filters - $ref: "../../models/directory/updated-at.yaml"
- refreshing - $ref: "../../models/directory/created-at.yaml"
- thumb - $ref: "../../models/directory/scanned-at.yaml"
- key - $ref: "../../models/directory/content.yaml"
- type - $ref: "../../models/directory/directory.yaml"
- title - $ref: "../../models/directory/content-changed-at.yaml"
- agent - $ref: "../../models/directory/hidden.yaml"
- scanner - type: object
- language properties:
- uuid Location:
- updatedAt type: array
- createdAt items:
- scannedAt type: object
- content required:
- directory - id
- contentChangedAt - path
- hidden properties:
- Location id:
properties: type: integer
allowSync: format: int32
type: boolean description: The ID of the location.
example: true example: 1
art: path:
type: string type: string
example: /:/resources/movie-fanart.jpg description: The path to the media item.
composite: example: /Movies
type: string
example: /library/sections/1/composite/1705615584
filters:
type: boolean
example: true
refreshing:
type: boolean
example: false
thumb:
type: string
example: /:/resources/movie.png
key:
type: string
example: "1"
type:
type: string
example: movie
title:
type: string
example: Movies
agent:
type: string
example: tv.plex.agents.movie
scanner:
type: string
example: Plex Movie
language:
type: string
example: en-US
uuid:
type: string
example: 322a231a-b7f7-49f5-920f-14c61199cd30
updatedAt:
$ref: "../../models/common/PlexDateTime.yaml"
createdAt:
$ref: "../../models/common/PlexDateTime.yaml"
scannedAt:
$ref: "../../models/common/PlexDateTime.yaml"
content:
type: boolean
example: true
directory:
type: boolean
example: true
contentChangedAt:
$ref: "../../models/common/PlexDateTime.yaml"
hidden:
type: integer
format: int32
example: 0
Location:
type: array
items:
type: object
required:
- id
- path
properties:
id:
type: integer
format: int32
example: 1
path:
type: string
example: /movies
"400": "400":
$ref: "../../responses/400.yaml" $ref: "../../responses/400.yaml"
"401": "401":

View File

@@ -1,4 +1,9 @@
import { validateResponseSpec } from "@utils" import {
randPlexUnixEpoch,
randRelativeSeconds,
randUUID,
validateResponseSpec
} from "@utils"
import { describe, it } from "vitest" import { describe, it } from "vitest"
describe("GET /library/sections", () => { describe("GET /library/sections", () => {
@@ -22,13 +27,13 @@ describe("GET /library/sections", () => {
agent: "tv.plex.agents.movie", agent: "tv.plex.agents.movie",
scanner: "Plex Movie", scanner: "Plex Movie",
language: "en-US", language: "en-US",
uuid: "a1b2c3d4e5f67890", uuid: randUUID(),
updatedAt: 1728394001, updatedAt: randPlexUnixEpoch(),
createdAt: 1598476504, createdAt: randPlexUnixEpoch(),
scannedAt: 1893047123, scannedAt: randPlexUnixEpoch(),
content: true, content: true,
directory: true, directory: true,
contentChangedAt: 4738921, contentChangedAt: randRelativeSeconds(),
hidden: 0, hidden: 0,
Location: [ Location: [
{ {
@@ -50,13 +55,13 @@ describe("GET /library/sections", () => {
agent: "tv.plex.agents.movie", agent: "tv.plex.agents.movie",
scanner: "Plex Movie", scanner: "Plex Movie",
language: "nl-NL", language: "nl-NL",
uuid: "b2c3d4e5f67890a1", uuid: randUUID(),
updatedAt: 1680007500, updatedAt: randPlexUnixEpoch(),
createdAt: 1680007500, createdAt: randPlexUnixEpoch(),
scannedAt: 1893047124, scannedAt: randPlexUnixEpoch(),
content: true, content: true,
directory: true, directory: true,
contentChangedAt: 5283714, contentChangedAt: randRelativeSeconds(),
hidden: 0, hidden: 0,
Location: [ Location: [
{ {
@@ -78,13 +83,13 @@ describe("GET /library/sections", () => {
agent: "tv.plex.agents.movie", agent: "tv.plex.agents.movie",
scanner: "Plex Movie", scanner: "Plex Movie",
language: "en-US", language: "en-US",
uuid: "c3d4e5f67890a1b2", uuid: randUUID(),
updatedAt: 1728394005, updatedAt: randPlexUnixEpoch(),
createdAt: 1598476200, createdAt: randPlexUnixEpoch(),
scannedAt: 1893047130, scannedAt: randPlexUnixEpoch(),
content: true, content: true,
directory: true, directory: true,
contentChangedAt: 6379184, contentChangedAt: randRelativeSeconds(),
hidden: 0, hidden: 0,
Location: [ Location: [
{ {
@@ -106,13 +111,13 @@ describe("GET /library/sections", () => {
agent: "tv.plex.agents.movie", agent: "tv.plex.agents.movie",
scanner: "Plex Movie", scanner: "Plex Movie",
language: "en-US", language: "en-US",
uuid: "d4e5f67890a1b2c3", uuid: randUUID(),
updatedAt: 1728394010, updatedAt: randPlexUnixEpoch(),
createdAt: 1598476302, createdAt: randPlexUnixEpoch(),
scannedAt: 1893047135, scannedAt: randPlexUnixEpoch(),
content: true, content: true,
directory: true, directory: true,
contentChangedAt: 5293874, contentChangedAt: randRelativeSeconds(),
hidden: 0, hidden: 0,
Location: [ Location: [
{ {
@@ -138,13 +143,13 @@ describe("GET /library/sections", () => {
agent: "tv.plex.agents.movie", agent: "tv.plex.agents.movie",
scanner: "Plex Movie", scanner: "Plex Movie",
language: "en-US", language: "en-US",
uuid: "e5f67890a1b2c3d4", uuid: randUUID(),
updatedAt: 1689075000, updatedAt: randPlexUnixEpoch(),
createdAt: 1689075000, createdAt: randPlexUnixEpoch(),
scannedAt: 1893047145, scannedAt: randPlexUnixEpoch(),
content: true, content: true,
directory: true, directory: true,
contentChangedAt: 5182738, contentChangedAt: randRelativeSeconds(),
hidden: 0, hidden: 0,
Location: [ Location: [
{ {
@@ -166,13 +171,13 @@ describe("GET /library/sections", () => {
agent: "com.plexapp.agents.hama", agent: "com.plexapp.agents.hama",
scanner: "Plex Series Scanner", scanner: "Plex Series Scanner",
language: "en", language: "en",
uuid: "f67890a1b2c3d4e5", uuid: randUUID(),
updatedAt: 1684970001, updatedAt: randPlexUnixEpoch(),
createdAt: 1598476000, createdAt: randPlexUnixEpoch(),
scannedAt: 1893047110, scannedAt: randPlexUnixEpoch(),
content: true, content: true,
directory: true, directory: true,
contentChangedAt: 8379201, contentChangedAt: randRelativeSeconds(),
hidden: 0, hidden: 0,
Location: [ Location: [
{ {
@@ -194,13 +199,13 @@ describe("GET /library/sections", () => {
agent: "tv.plex.agents.series", agent: "tv.plex.agents.series",
scanner: "Plex TV Series", scanner: "Plex TV Series",
language: "nl-NL", language: "nl-NL",
uuid: "67890a1b2c3d4e5f", uuid: randUUID(),
updatedAt: 1728394002, updatedAt: randPlexUnixEpoch(),
createdAt: 1680007400, createdAt: randPlexUnixEpoch(),
scannedAt: 1893047125, scannedAt: randPlexUnixEpoch(),
content: true, content: true,
directory: true, directory: true,
contentChangedAt: 5948203, contentChangedAt: randRelativeSeconds(),
hidden: 0, hidden: 0,
Location: [ Location: [
{ {
@@ -222,13 +227,13 @@ describe("GET /library/sections", () => {
agent: "tv.plex.agents.series", agent: "tv.plex.agents.series",
scanner: "Plex TV Series", scanner: "Plex TV Series",
language: "nl-NL", language: "nl-NL",
uuid: "890a1b2c3d4e5f67", uuid: randUUID(),
updatedAt: 1728394007, updatedAt: randPlexUnixEpoch(),
createdAt: 1601860600, createdAt: randPlexUnixEpoch(),
scannedAt: 1893047145, scannedAt: randPlexUnixEpoch(),
content: true, content: true,
directory: true, directory: true,
contentChangedAt: 6283720, contentChangedAt: randRelativeSeconds(),
hidden: 0, hidden: 0,
Location: [ Location: [
{ {
@@ -250,13 +255,13 @@ describe("GET /library/sections", () => {
agent: "tv.plex.agents.series", agent: "tv.plex.agents.series",
scanner: "Plex TV Series", scanner: "Plex TV Series",
language: "en-US", language: "en-US",
uuid: "a1b2c3d4e5f67890", uuid: randUUID(),
updatedAt: 1728394003, updatedAt: randPlexUnixEpoch(),
createdAt: 1598476100, createdAt: randPlexUnixEpoch(),
scannedAt: 1893047150, scannedAt: randPlexUnixEpoch(),
content: true, content: true,
directory: true, directory: true,
contentChangedAt: 6472184, contentChangedAt: randRelativeSeconds(),
hidden: 0, hidden: 0,
Location: [ Location: [
{ {
@@ -278,13 +283,13 @@ describe("GET /library/sections", () => {
agent: "tv.plex.agents.series", agent: "tv.plex.agents.series",
scanner: "Plex TV Series", scanner: "Plex TV Series",
language: "en-US", language: "en-US",
uuid: "b2c3d4e5f67890a1", uuid: randUUID(),
updatedAt: 1689076000, updatedAt: randPlexUnixEpoch(),
createdAt: 1689076000, createdAt: randPlexUnixEpoch(),
scannedAt: 1893047155, scannedAt: randPlexUnixEpoch(),
content: true, content: true,
directory: true, directory: true,
contentChangedAt: 4920835, contentChangedAt: randRelativeSeconds(),
hidden: 0, hidden: 0,
Location: [ Location: [
{ {
@@ -306,13 +311,13 @@ describe("GET /library/sections", () => {
agent: "tv.plex.agents.series", agent: "tv.plex.agents.series",
scanner: "Plex TV Series", scanner: "Plex TV Series",
language: "en-US", language: "en-US",
uuid: "c3d4e5f67890a1b2", uuid: randUUID(),
updatedAt: 1689077000, updatedAt: randPlexUnixEpoch(),
createdAt: 1689077000, createdAt: randPlexUnixEpoch(),
scannedAt: 1893047155, scannedAt: randPlexUnixEpoch(),
content: true, content: true,
directory: true, directory: true,
contentChangedAt: 5309283, contentChangedAt: randRelativeSeconds(),
hidden: 0, hidden: 0,
Location: [ Location: [
{ {
@@ -334,13 +339,13 @@ describe("GET /library/sections", () => {
agent: "tv.plex.agents.series", agent: "tv.plex.agents.series",
scanner: "Plex TV Series", scanner: "Plex TV Series",
language: "en-US", language: "en-US",
uuid: "d4e5f67890a1b2c3", uuid: randUUID(),
updatedAt: 1689078000, updatedAt: randPlexUnixEpoch(),
createdAt: 1626704821, createdAt: randPlexUnixEpoch(),
scannedAt: 1893047170, scannedAt: randPlexUnixEpoch(),
content: true, content: true,
directory: true, directory: true,
contentChangedAt: 7291885, contentChangedAt: randRelativeSeconds(),
hidden: 0, hidden: 0,
Location: [ Location: [
{ {
@@ -362,13 +367,13 @@ describe("GET /library/sections", () => {
agent: "tv.plex.agents.music", agent: "tv.plex.agents.music",
scanner: "Plex Music", scanner: "Plex Music",
language: "en-US", language: "en-US",
uuid: "e5f67890a1b2c3d4", uuid: randUUID(),
updatedAt: 1684974922, updatedAt: randPlexUnixEpoch(),
createdAt: 1598476740, createdAt: randPlexUnixEpoch(),
scannedAt: 1893047140, scannedAt: randPlexUnixEpoch(),
content: true, content: true,
directory: true, directory: true,
contentChangedAt: 7204063, contentChangedAt: randRelativeSeconds(),
hidden: 0, hidden: 0,
Location: [ Location: [
{ {
@@ -390,13 +395,13 @@ describe("GET /library/sections", () => {
agent: "com.plexapp.agents.none", agent: "com.plexapp.agents.none",
scanner: "Plex Video Files Scanner", scanner: "Plex Video Files Scanner",
language: "xn", language: "xn",
uuid: "f67890a1b2c3d4e5", uuid: randUUID(),
updatedAt: 1684974733, updatedAt: randPlexUnixEpoch(),
createdAt: 1598475949, createdAt: randPlexUnixEpoch(),
scannedAt: 1893047123, scannedAt: randPlexUnixEpoch(),
content: true, content: true,
directory: true, directory: true,
contentChangedAt: 3828909, contentChangedAt: randRelativeSeconds(),
hidden: 0, hidden: 0,
Location: [ Location: [
{ {
@@ -409,6 +414,138 @@ describe("GET /library/sections", () => {
} }
} }
validateResponseSpec("/library/sections", "get", 200, response)
})
it("should validate the 200 response when the API spec is valid", () => {
const response = {
MediaContainer: {
size: 5,
allowSync: false,
title1: "Plex Library",
Directory: [
{
allowSync: false,
art: "/:/resources/movie-fanart.jpg",
composite: "/library/sections/4/composite/1743766611",
filters: true,
refreshing: false,
thumb: "/:/resources/movie.png",
key: "4",
type: "movie",
title: "Kids Movies",
agent: "tv.plex.agents.movie",
scanner: "Plex Movie",
language: "en-US",
uuid: randUUID(),
updatedAt: randPlexUnixEpoch(),
createdAt: randPlexUnixEpoch(),
scannedAt: randPlexUnixEpoch(),
content: true,
directory: true,
contentChangedAt: randRelativeSeconds(),
hidden: 0,
Location: [{ id: 50, path: "/mnt/Media/Kids/Movies" }]
},
{
allowSync: false,
art: "/:/resources/movie-fanart.jpg",
composite: "/library/sections/3/composite/1743769012",
filters: true,
refreshing: false,
thumb: "/:/resources/movie.png",
key: "3",
type: "movie",
title: "Movies",
agent: "tv.plex.agents.movie",
scanner: "Plex Movie",
language: "en-US",
uuid: randUUID(),
updatedAt: randPlexUnixEpoch(),
createdAt: randPlexUnixEpoch(),
scannedAt: randPlexUnixEpoch(),
content: true,
directory: true,
contentChangedAt: randRelativeSeconds(),
hidden: 0,
Location: [{ id: 31, path: "/mnt/Movies_1" }]
},
{
allowSync: false,
art: "/:/resources/show-fanart.jpg",
composite: "/library/sections/13/composite/1743768412",
filters: true,
refreshing: false,
thumb: "/:/resources/show.png",
key: "13",
type: "show",
title: "Kids TV Shows",
agent: "tv.plex.agents.series",
scanner: "Plex TV Series",
language: "en-US",
uuid: randUUID(),
updatedAt: randPlexUnixEpoch(),
createdAt: randPlexUnixEpoch(),
scannedAt: randPlexUnixEpoch(),
content: true,
directory: true,
contentChangedAt: randRelativeSeconds(),
hidden: 0,
Location: [{ id: 51, path: "/mnt/Media/Kids/TV Shows" }]
},
{
allowSync: false,
art: "/:/resources/show-fanart.jpg",
composite: "/library/sections/1/composite/1743772322",
filters: true,
refreshing: false,
thumb: "/:/resources/show.png",
key: "1",
type: "show",
title: "TV Shows",
agent: "tv.plex.agents.series",
scanner: "Plex TV Series",
language: "en-US",
uuid: randUUID(),
updatedAt: randPlexUnixEpoch(),
createdAt: randPlexUnixEpoch(),
scannedAt: randPlexUnixEpoch(),
content: true,
directory: true,
contentChangedAt: randRelativeSeconds(),
hidden: 0,
Location: [
{ id: 59, path: "/mnt/TV_3" },
{ id: 19, path: "/mnt/TV_1" },
{ id: 20, path: "/mnt/TV_2" }
]
},
{
allowSync: false,
art: "/:/resources/artist-fanart.jpg",
composite: "/library/sections/5/composite/1743766611",
filters: true,
refreshing: false,
thumb: "/:/resources/artist.png",
key: "5",
type: "artist",
title: "Audiobooks",
agent: "tv.plex.agents.music",
scanner: "Plex Music",
language: "en-US",
uuid: randUUID(),
updatedAt: randPlexUnixEpoch(),
createdAt: randPlexUnixEpoch(),
scannedAt: randPlexUnixEpoch(),
content: true,
directory: true,
contentChangedAt: randRelativeSeconds(),
hidden: 1,
Location: [{ id: 49, path: "/mnt/Media/Audiobooks" }]
}
]
}
}
validateResponseSpec("/library/sections", "get", 200, response) validateResponseSpec("/library/sections", "get", 200, response)
}) })
}) })

View File

@@ -2,7 +2,7 @@ import PMSSpec from "../../output/plex-media-server-spec-dereferenced.yaml"
import Ajv from "ajv" import Ajv from "ajv"
import addFormats from "ajv-formats" import addFormats from "ajv-formats"
import { expect } from "vitest" import { expect } from "vitest"
import { merge } from "lodash-es" import { isArray, merge } from "lodash-es"
import { xml2json } from "xml-js" import { xml2json } from "xml-js"
/** /**
* Validate a response against the OpenAPI spec * Validate a response against the OpenAPI spec
@@ -57,7 +57,19 @@ export function validateResponseSpec(
) )
if (!validate) { if (!validate) {
for (const error of ajv.errors) {
if (
error.hasOwnProperty("params") &&
error.params.hasOwnProperty("allowedValues") &&
isArray(error.params.allowedValues)
) {
// Format the allowedValues to be a string to make it visible in the error message
error.params.allowedValues = error.params.allowedValues.join(", ")
}
}
console.error(ajv.errors) console.error(ajv.errors)
console.error(JSON.stringify(jsonResponse, null, 2))
} }
expect(ajv.errors).toBe(null) expect(ajv.errors).toBe(null)

View File

@@ -1 +1,2 @@
export * from "./import.js" export * from "./import.js"
export * from "./mocks.js"

14
tests/utils/mocks.ts Normal file
View File

@@ -0,0 +1,14 @@
import { randNumber, randRecentDate, randUuid } from "@ngneat/falso"
export function randUUID() {
return randUuid()
}
export function randPlexUnixEpoch() {
const date = randRecentDate()
return Math.floor(date.getTime() / 1000)
}
export function randRelativeSeconds() {
return randNumber({ min: 1000, max: 100000 })
}