Merge pull request #9 from LukeHagar/liblab-codegen-1698372724049

liblab SDK update
This commit is contained in:
Luke Hagar
2023-10-26 21:14:29 -05:00
committed by GitHub
139 changed files with 16223 additions and 92 deletions

View File

@@ -0,0 +1,12 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
{
"name": "TypeScript SDK",
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye",
"postCreateCommand": "cd examples && npm run setup",
"customizations": {
"codespaces":{
"openFiles": ["examples/src/index.ts", "README.md"]
}
}
}

50
.eslintrc.json Normal file
View File

@@ -0,0 +1,50 @@
{
"env": {
"browser": true,
"commonjs": true,
"es2021": true
},
"extends": [
"airbnb-base",
"airbnb-typescript/base",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"project": "./tsconfig.eslint.json"
},
"plugins": [
"@typescript-eslint",
"prettier"
],
"rules": {
"no-console": "off",
"max-len": [
"error",
{
"code": 150,
"ignoreComments": true,
"ignoreRegExpLiterals": true,
"ignoreStrings": true,
"ignoreTemplateLiterals": true
}
],
"prettier/prettier": "error",
"@typescript-eslint/dot-notation": "off",
"import/prefer-default-export": "off"
},
"settings": {
"import/resolver": {
"node": {
"extensions": [
".js",
".jsx",
".ts",
".tsx"
]
}
}
}
}

1
.github/PROTECTED_BRANCHES vendored Normal file
View File

@@ -0,0 +1 @@
main

35
.github/workflows/build-checks.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Release Checks
on:
push:
branches:
- main
jobs:
github-publish:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository & Submo
uses: actions/checkout@v2
with:
submodules: recursive
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Install dependencies
run: npm install
# TODO: Finish fixing eslint issues
# - name: Run ESLint check
# run: npm run lint:ci
- name: Run Test & Coverage check
run: npm run test

28
.github/workflows/pr-checks.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Pull Request Checks
on: [pull_request]
jobs:
linting-and-testing:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Install dependencies
run: npm install
# TODO: Finish fixing eslint issues
# - name: Run ESLint check
# run: npm run lint:ci
- name: Run Test & Coverage check
run: npm run test

32
.github/workflows/release-checks.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Release Checks
on:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
- run: npm ci
- run: npm run test
npm-publish:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

135
.gitignore vendored
View File

@@ -1,5 +1,132 @@
node_modules
build
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
api-specs
.env
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.parcel-cache

26
.prettierrc.json Normal file
View File

@@ -0,0 +1,26 @@
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "always",
"overrides": [
{
"files": ".editorconfig",
"options": {
"parser": "yaml"
}
},
{
"files": "LICENSE",
"options": {
"parser": "markdown"
}
}
]
}

19
LICENSE Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2023
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.

2035
README.md Normal file

File diff suppressed because it is too large Load Diff

1
examples/.env.example Normal file
View File

@@ -0,0 +1 @@
PLEXSDK_TOKEN=

View File

@@ -1,43 +1,23 @@
# 🧰 Simple TypeScript Starter | 2022
# @lukehagar/plexjs-example
A basic example of how to use the @lukehagar/plexjs package.
> We talk about a lot of **advanced Node.js and TypeScript** concepts on [the blog](https://khalilstemmler.com), particularly focused around Domain-Driven Design and large-scale enterprise application patterns. However, I received a few emails from readers that were interested in seeing what a basic TypeScript starter project looks like. So I've put together just that.
## Installation
### Features
In the event `@lukehagar/plexjs` is not published to npm, you can install it locally by running the following command in the examples folder:
```sh
npm run setup
```
- Minimal
- TypeScript v4
- Testing with Jest
- Linting with Eslint and Prettier
- Pre-commit hooks with Husky
- VS Code debugger scripts
- Local development with Nodemon
This will rebuild the parent package and install it locally.
### Scripts
Otherwise you can install it from npm:
```sh
npm install @lukehagar/plexjs
```
#### `npm run start:dev`
## Usage
Starts the application in development using `nodemon` and `ts-node` to do hot reloading.
#### `npm run start`
Starts the app in production by first building the project with `npm run build`, and then executing the compiled JavaScript at `build/index.js`.
#### `npm run build`
Builds the app at `build`, cleaning the folder first.
#### `npm run test`
Runs the `jest` tests once.
#### `npm run test:dev`
Run the `jest` tests in watch mode, waiting for file changes.
#### `npm run prettier-format`
Format your code.
#### `npm run prettier-watch`
Format your code in watch mode, waiting for file changes.
To run the example, run the following command in the examples folder:
```sh
npm run start
```

View File

@@ -1,31 +1,18 @@
{
"name": "typescript-starter",
"name": "@lukehagar/plexjs-example",
"version": "1.0.0",
"description": "A basic typescript app starter for 2023.",
"main": "index.js",
"scripts": {
"build": "rimraf ./build && tsc",
"dev": "npx nodemon",
"start": "npm run build && node build/index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@lukehagar/plexjs": "^0.0.27",
"@types/jest": "^29.5.3",
"@types/node": "^20.4.2",
"@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0",
"dotenv": "^16.3.1",
"nodemon": "^3.0.1",
"onchange": "^7.1.0",
"rimraf": "^5.0.1",
"run-script-os": "^1.1.6",
"ts-node": "^10.9.1",
"typescript": "^5.1.6"
},
"private": true,
"dependencies": {
"jest-cucumber": "^3.0.1"
"@lukehagar/plexjs": "file:../"
},
"scripts": {
"setup": "npm --prefix ../ install && npm --prefix ../ run build && npm install",
"start": "tsc && node -r dotenv/config dist/index.js",
"dev": "ts-node src/index.ts"
},
"devDependencies": {
"ts-node": "^10.9.1",
"typescript": "4.8.4",
"dotenv": "^8.2.0"
}
}

View File

@@ -1,14 +1,8 @@
import { Configuration, ServerApi, PlexTvApi } from "@lukehagar/plexjs";
import dotenv from "dotenv";
dotenv.config();
import { PlexSDK } from '@lukehagar/plexjs';
const config = new Configuration({
basePath: process.env.BASE_PATH,
plexToken: process.env.PLEX_TOKEN,
});
const sdk = new PlexSDK({ apiKey: process.env.PLEXSDK_API_KEY_TOKEN });
new ServerApi(config).getServerCapabilities().then((resp) => console.log(resp));
new PlexTvApi(config).getDevices().then((resp) => console.log(resp));
new PlexTvApi(config).getUserDetails().then((resp) => console.log(resp));
(async () => {
const result = await sdk.server.getServerCapabilities();
console.log(result.data);
})();

View File

@@ -1,16 +1,103 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": ["es6"],
"allowJs": true,
"outDir": "build",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"types": ["node", "jest"],
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["src/**/*.spec.ts"]
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

2
install.sh Normal file
View File

@@ -0,0 +1,2 @@
npm install
npm run test

4
jest.config.json Normal file
View File

@@ -0,0 +1,4 @@
{
"preset": "ts-jest",
"testEnvironment": "node"
}

6197
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

57
package.json Normal file
View File

@@ -0,0 +1,57 @@
{
"source": "./src/index.ts",
"exports": {
"require": "./dist/commonjs/index.js",
"types": "./dist/commonjs/index.d.ts",
"default": "./dist/esm/index.js"
},
"main": "./dist/commonjs/index.js",
"module": "./dist/esm/index.js",
"browser": "./dist/index.umd.js",
"unpkg": "./dist/index.umd.js",
"types": "./dist/commonjs/index.d.ts",
"files": [
"dist",
"README.md"
],
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0",
"@types/node": "^17.0.23",
"@types/jest": "^29.5.6",
"eslint": "^8.20.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^29.7.0",
"nock": "^13.2.4",
"prettier": "^2.6.2",
"ts-jest": "^29.1.1",
"typescript": "4.8.4"
},
"scripts": {
"build": "npm run build:all",
"build:cjs": "tsc --module commonjs --outDir dist/commonjs",
"build:esm": "tsc --module esnext --outDir dist/esm",
"build:umd": "tsc --module umd --outDir dist/umd",
"build:all": "npm run build:cjs && npm run build:esm && npm run build:umd",
"lint": "eslint --ext .ts,.js ./src/ --resolve-plugins-relative-to .",
"lint:ci": "eslint --ext .ts,.js ./src/ --resolve-plugins-relative-to . --cache --quiet",
"lint:fix": "eslint --ext .ts,.js ./src/ --resolve-plugins-relative-to . --cache --fix",
"rebuild": "rm -rf dist/ && tsc",
"test": "jest --detectOpenHandles",
"watch": "rm -rf dist/ && tsc -w",
"version": "tsc --version",
"prepublishOnly": "npm run build"
},
"name": "@lukehagar/plexjs",
"description": "PlexSDK - An Open API Spec for interacting with Plex.tv and Plex Servers",
"version": "0.0.1",
"author": "Luke Hagar <lukeslakemail@gmail.com>",
"dependencies": {
"axios": "^1.5.1"
},
"license": "MIT"
}

47
src/BaseService.ts Normal file
View File

@@ -0,0 +1,47 @@
import { Environment } from './http/Environment';
import HTTPLibrary from './http/HTTPLibrary';
import { Headers } from './http/HTTPClient';
export default class BaseService {
public baseUrl: string = Environment.DEFAULT;
public httpClient = new HTTPLibrary();
private apiKey: string = '';
private apiKeyHeader: string = 'X-Plex-Token';
setApiKey(key: string, header: string = 'X-Plex-Token'): void {
this.apiKey = key;
this.apiKeyHeader = header;
}
getAuthorizationHeader(): Headers {
const apiKeyAuth = { [this.apiKeyHeader]: this.apiKey };
return { ...apiKeyAuth };
}
setBaseUrl(url: string): void {
this.baseUrl = url;
}
constructor(apiKey: string = '', apiKeyHeader: string = 'X-Plex-Token') {
this.setApiKey(apiKey, apiKeyHeader);
}
static patternMatching(value: string, pattern: string, variableName: string): string {
if (!value) {
throw new Error(`${variableName} cannot be null or undefined`);
}
if (!value.match(new RegExp(pattern))) {
throw new Error(`Invalid value for ${variableName}: must match ${pattern}`);
}
return value;
}
static urlEncode = (input: { [key: string]: any }): string =>
Object.keys(input)
.map((key) => `${key}=${encodeURIComponent(input[key])}`)
.join('&');
}

28
src/hooks/Hook.ts Normal file
View File

@@ -0,0 +1,28 @@
export interface Request {
method: string;
url: string;
input?: object;
headers: object;
}
export interface Response {
data: object;
headers: object;
status: number;
}
export interface Exception extends Error {
title: string;
type?: string;
detail?: string;
instance?: string;
statusCode: number;
}
export interface Hook {
beforeRequest(request: Request): Promise<void>;
afterResponse(request: Request, response: Response): Promise<void>;
onError(error: Exception): Promise<void>;
}

3
src/http/Environment.ts Normal file
View File

@@ -0,0 +1,3 @@
export enum Environment {
DEFAULT = '{protocol}://{ip}:{port}',
}

12
src/http/HTTPClient.ts Normal file
View File

@@ -0,0 +1,12 @@
export interface Headers extends Record<string, string> {}
/**
* Defines the basic operations for an HTTP client.
*/
export default interface HTTPClient {
get(url: string, input: any, headers: Headers, retry?: boolean): Promise<any>;
post(url: string, input: any, headers: Headers, retry?: boolean): Promise<any>;
delete(url: string, input: any, headers: Headers, retry?: boolean): Promise<any>;
put(url: string, input: any, headers: Headers, retry?: boolean): Promise<any>;
patch(url: string, input: any, headers: Headers, retry?: boolean): Promise<any>;
}

161
src/http/HTTPLibrary.ts Normal file
View File

@@ -0,0 +1,161 @@
import axios, { AxiosError } from 'axios';
import HTTPClient, { Headers } from './HTTPClient';
import throwHttpError from './httpExceptions';
export default class HTTPLibrary implements HTTPClient {
readonly userAgentHeader: Headers = {
'User-Agent': 'liblab/0.1.25 PlexSDK/0.0.1 typescript/5.2.2',
};
readonly retryAttempts: number = 3;
readonly retryDelayMs: number = 150;
private static readonly responseMapper: Map<string, string> = new Map<string, string>([
['type', 'type_'],
['default', 'default_'],
]);
private readonly requestMapper: Map<string, string> = new Map<string, string>([
['type_', 'type'],
['default_', 'default'],
]);
async get(url: string, input: any, headers: Headers, retry: boolean = false): Promise<any> {
const request = () =>
axios.get(url, {
headers: { ...headers, ...this.getUserAgentHeader() },
data:
Object.keys(input).length > 0
? HTTPLibrary.convertKeysWithMapper(input, this.requestMapper)
: undefined,
});
const response = retry
? await this.retry(this.retryAttempts, request, this.retryDelayMs)
: await request();
return HTTPLibrary.handleResponse(response);
}
async post(url: string, input: any, headers: Headers, retry: boolean = false): Promise<any> {
const request = () =>
axios.post(url, HTTPLibrary.convertKeysWithMapper(input, this.requestMapper), {
headers: { ...headers, ...this.getUserAgentHeader() },
});
const response = retry
? await this.retry(this.retryAttempts, request, this.retryDelayMs)
: await request();
return HTTPLibrary.handleResponse(response);
}
async delete(url: string, input: any, headers: Headers, retry: boolean = false): Promise<any> {
const request = () =>
axios.delete(url, {
headers: { ...headers, ...this.getUserAgentHeader() },
data: HTTPLibrary.convertKeysWithMapper(input, this.requestMapper),
});
const response = retry
? await this.retry(this.retryAttempts, request, this.retryDelayMs)
: await request();
return HTTPLibrary.handleResponse(response);
}
async put(url: string, input: any, headers: Headers, retry: boolean = false): Promise<any> {
const request = () =>
axios.put(url, HTTPLibrary.convertKeysWithMapper(input, this.requestMapper), {
headers: { ...headers, ...this.getUserAgentHeader() },
});
const response = retry
? await this.retry(this.retryAttempts, request, this.retryDelayMs)
: await request();
return HTTPLibrary.handleResponse(response);
}
async patch(url: string, input: any, headers: Headers, retry: boolean = false): Promise<any> {
const request = () =>
axios.patch(url, HTTPLibrary.convertKeysWithMapper(input, this.requestMapper), {
headers: { ...headers, ...this.getUserAgentHeader() },
});
const response = retry
? await this.retry(this.retryAttempts, request, this.retryDelayMs)
: await request();
return HTTPLibrary.handleResponse(response);
}
async retry(retries: number, callbackFn: () => any, delay: number): Promise<any> {
let result: any;
try {
result = await callbackFn();
} catch (e: any) {
if ((e as AxiosError).isAxiosError) {
if (e.response) {
if (![500, 503, 504].includes(e.response.status)) {
return e.response;
}
}
}
if (retries > 1) {
// eslint-disable-next-line no-promise-executor-return
await new Promise((resolve) => setTimeout(resolve, delay));
result = await this.retry(retries - 1, callbackFn, delay * 2);
} else {
throw e;
}
}
return result;
}
private static handleResponse(response: any) {
if (response.status >= 400) {
throwHttpError(response);
}
response.data = HTTPLibrary.convertKeysWithMapper(response.data, this.responseMapper);
return response;
}
private getUserAgentHeader(): Headers {
if (typeof window !== 'undefined') {
return {};
}
return this.userAgentHeader;
}
/**
*Converts keys in an object using a provided JSON mapper.
* @param {any} obj - The object to convert keys for.
* @param {Object} jsonMapper - The JSON mapper containing key mappings.
* @returns {any} - The object with converted keys.
*/
private static convertKeysWithMapper<T>(obj: T, jsonMapper: Map<string, string>): any {
if (!obj || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => HTTPLibrary.convertKeysWithMapper(item, jsonMapper));
}
const convertedObj: Record<string, any> = {};
Object.entries(obj).forEach(([key, value]) => {
if (value !== undefined) {
const convertedKey = jsonMapper.get(key) || key;
convertedObj[convertedKey] = HTTPLibrary.convertKeysWithMapper(value, jsonMapper);
}
});
return convertedObj;
}
}

View File

@@ -0,0 +1,82 @@
export type Explode = boolean;
export type QueryStyles = 'form' | 'spaceDelimited' | 'pipeDelimited' | 'deepObject';
export type PathStyles = 'simple' | 'label' | 'matrix';
const styleMethods: Record<string, Function> = {
simple: (value: unknown, explode: boolean) => {
// Check if the value is an array
if (Array.isArray(value)) {
return explode ? value.join(',') : value.join();
}
// Check if the value is an object
if (typeof value === 'object' && value !== null) {
if (explode) {
// Serialize object with exploded format: "key=value,key2=value2"
return Object.entries(value)
.map(([parameterName, parameterValue]) => `${parameterName}=${parameterValue}`)
.join(',');
}
// Serialize object with non-exploded format: "key,value,key2,value2"
return Object.entries(value)
.flatMap(([parameterName, parameterValue]) => [parameterName, parameterValue])
.join(',');
}
// For primitive values
return String(value);
},
form: (parameterName: string, parameterValue: unknown, explode: boolean) => {
// Check if the parameterValue is an array
if (Array.isArray(parameterValue)) {
return explode
? parameterValue.map((value) => `${parameterName}=${value}`).join('&')
: `${parameterName}=${parameterValue.join(',')}`;
}
// Check if the parameterValue is an object
if (typeof parameterValue === 'object' && parameterValue !== null) {
if (explode) {
// Serialize object with exploded format: "key1=value1&key2=value2"
return Object.entries(parameterValue)
.map(([name, value]) => `${name}=${value}`)
.join('&');
}
// Serialize object with non-exploded format: "key=key1,value1,key2,value2"
return `${parameterName}=${Object.entries(parameterValue)
.flatMap(([name, value]) => [name, value])
.join(',')}`;
}
// For primitive values
return `${parameterName}=${parameterValue}`;
},
};
export function serializeQuery(
style: QueryStyles,
explode: Explode,
key: string,
value: unknown,
): string {
const method = styleMethods[style];
if (!method) return '';
return method(key, value, explode);
}
export function serializePath(
style: PathStyles,
explode: Explode,
value: unknown,
key?: string,
): string {
const method = styleMethods[style];
if (!method) return '';
// The `simple` and `label` styles do not require a `key`
if (!key) {
return method(value, explode);
} else {
return method(key, value, explode);
}
}

4
src/http/Response.ts Normal file
View File

@@ -0,0 +1,4 @@
export default interface Response<T> {
data: T;
headers: Record<string, string>;
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class BadGateway extends BaseHTTPError {
statusCode = 502;
title = 'Bad Gateway';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class BadRequest extends BaseHTTPError {
statusCode = 400;
title = 'Bad Request';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class Conflict extends BaseHTTPError {
statusCode = 409;
title = 'Conflict';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class ExpectationFailed extends BaseHTTPError {
statusCode = 417;
title = 'Expectation Failed';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class FailedDependency extends BaseHTTPError {
statusCode = 424;
title = 'Failed Dependency';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class Forbidden extends BaseHTTPError {
statusCode = 403;
title = 'Forbidden';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class GatewayTimeout extends BaseHTTPError {
statusCode = 504;
title = 'Gateway Timeout';
constructor(detail: string = '') {
super(detail);
}
}

11
src/http/errors/Gone.ts Normal file
View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class Gone extends BaseHTTPError {
statusCode = 410;
title = 'Gone';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class HttpVersionNotSupported extends BaseHTTPError {
statusCode = 505;
title = 'HTTP Version Not Supported';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class InternalServerError extends BaseHTTPError {
statusCode = 500;
title = 'Internal Server Error';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class LengthRequired extends BaseHTTPError {
statusCode = 411;
title = 'LengthRequired';
constructor(detail: string = '') {
super(detail);
}
}

11
src/http/errors/Locked.ts Normal file
View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class Locked extends BaseHTTPError {
statusCode = 423;
title = 'Locked';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class LoopDetected extends BaseHTTPError {
statusCode = 508;
title = 'Loop Detected';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,14 @@
import { BaseHTTPError } from './base';
export default class MethodNotAllowed extends BaseHTTPError {
statusCode = 405;
title = 'Method Not Allowed';
allow?: string[];
constructor(detail: string = '', allow?: string[]) {
super(detail);
this.allow = allow;
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class MisdirectedRequest extends BaseHTTPError {
statusCode = 421;
title = 'Misdirected Request';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class NetworkAuthenticationRequired extends BaseHTTPError {
statusCode = 511;
title = 'Network Authentication Required';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class NotAcceptable extends BaseHTTPError {
statusCode = 406;
title = 'Not Acceptable';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class NotExtended extends BaseHTTPError {
statusCode = 510;
title = 'Not Extended';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class NotFound extends BaseHTTPError {
statusCode = 404;
title = 'Not Found';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class NotImplemented extends BaseHTTPError {
statusCode = 501;
title = 'Not Implemented';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,14 @@
import { BaseHTTPError } from './base';
export default class PayloadTooLarge extends BaseHTTPError {
statusCode = 413;
title = 'Payload Too Large';
retryAfter: number | null;
constructor(detail: string = '', retryAfter: number | null = null) {
super(detail);
this.retryAfter = retryAfter;
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class PaymentRequired extends BaseHTTPError {
statusCode = 402;
title = 'Payment Required';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class PreconditionFailed extends BaseHTTPError {
statusCode = 412;
title = 'PreconditionFailed';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class PreconditionRequired extends BaseHTTPError {
statusCode = 428;
title = 'Precondition Required';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,14 @@
import { AuthenticateChallenge, BaseHTTPError } from './base';
export default class ProxyAuthenticationRequired extends BaseHTTPError {
statusCode = 407;
title = 'Proxy Authentication Required';
proxyAuthenticate?: AuthenticateChallenge;
constructor(detail: string = '', proxyAuthenticate?: AuthenticateChallenge) {
super(detail);
this.proxyAuthenticate = proxyAuthenticate;
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class RangeNotSatisfiable extends BaseHTTPError {
statusCode = 416;
title = 'Range Not Satisfiable';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class RequestHeaderFieldsTooLarge extends BaseHTTPError {
statusCode = 431;
title = 'Request Header Fields Too Large';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class RequestTimeout extends BaseHTTPError {
statusCode = 408;
title = 'Request Timeout';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,14 @@
import { BaseHTTPError } from './base';
export default class ServiceUnavailable extends BaseHTTPError {
statusCode = 503;
title = 'Service Unavailable';
retryAfter: number | null;
constructor(detail: string = '', retryAfter: number | null = null) {
super(detail);
this.retryAfter = retryAfter;
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class TooEarly extends BaseHTTPError {
statusCode = 425;
title = 'Too Early';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,14 @@
import { BaseHTTPError } from './base';
export default class TooManyRequests extends BaseHTTPError {
statusCode = 429;
title = 'Too Many Requests';
retryAfter: number | null;
constructor(detail: string = '', retryAfter: number | null = null) {
super(detail);
this.retryAfter = retryAfter;
}
}

View File

@@ -0,0 +1,14 @@
import { AuthenticateChallenge, BaseHTTPError } from './base';
export default class Unauthorized extends BaseHTTPError {
statusCode = 401;
title = 'Unauthorized';
wwwAuthenticate?: AuthenticateChallenge;
constructor(detail: string = '', wwwAuthenticate?: AuthenticateChallenge) {
super(detail);
this.wwwAuthenticate = wwwAuthenticate;
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class UnavailableForLegalReasons extends BaseHTTPError {
statusCode = 451;
title = 'Unavailable For Legal Reasons';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class UnprocessableEntity extends BaseHTTPError {
statusCode = 422;
title = 'Unprocessable Entity';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class UnsufficientStorage extends BaseHTTPError {
statusCode = 507;
title = 'Unsufficient Storage';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class UnsupportedMediaType extends BaseHTTPError {
statusCode = 415;
title = 'Unsupported Media Type';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class UpgradeRequired extends BaseHTTPError {
statusCode = 426;
title = 'Upgrade Required';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class UriTooLong extends BaseHTTPError {
statusCode = 414;
title = 'URI Too Long';
constructor(detail: string = '') {
super(detail);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseHTTPError } from './base';
export default class VariantAlsoNegotiates extends BaseHTTPError {
statusCode = 506;
title = 'Variant Also Negotiates';
constructor(detail: string = '') {
super(detail);
}
}

52
src/http/errors/base.ts Normal file
View File

@@ -0,0 +1,52 @@
export interface IHTTPError extends Error {
statusCode: number;
}
export interface IHTTPErrorDescription extends IHTTPError {
type?: string;
title: string;
detail?: string;
instance?: string;
}
export function isHTTPError(error: unknown): error is IHTTPError {
if (!error) {
return false;
}
return Number.isInteger((error as IHTTPError).statusCode);
}
export function isHTTPIssue(error: unknown): error is IHTTPErrorDescription {
if (!error) {
return false;
}
return (error as IHTTPErrorDescription).title !== undefined && isHTTPError(error);
}
export class BaseHTTPError extends Error implements IHTTPError {
public type?: string;
public title: string = 'Internal Server Error';
public detail?: string;
public instance?: string;
public statusCode: number = 500;
constructor(detail: string = '') {
super(detail || 'An Unknown HTTP Error Occurred');
this.detail = detail;
this.stack = (<any>new Error()).stack;
}
}
export function isClientError(error: Error): boolean {
return isHTTPError(error);
}
export function isServerError(e: Error): boolean {
return isHTTPError(e) && e.statusCode >= 500 && e.statusCode <= 599;
}
export type AuthenticateChallenge = string | string[];

83
src/http/errors/index.ts Normal file
View File

@@ -0,0 +1,83 @@
import BadRequest from './BadRequest';
import Unauthorized from './Unauthorized';
import PaymentRequired from './PaymentRequired';
import Forbidden from './Forbidden';
import NotFound from './NotFound';
import MethodNotAllowed from './MethodNotAllowed';
import NotAcceptable from './NotAcceptable';
import ProxyAuthenticationRequired from './ProxyAuthenticationRequired';
import RequestTimeout from './RequestTimeout';
import Conflict from './Conflict';
import Gone from './Gone';
import LengthRequired from './LengthRequired';
import PreconditionFailed from './PreconditionFailed';
import PayloadTooLarge from './PayloadTooLarge';
import UriTooLong from './UriTooLong';
import UnsupportedMediaType from './UnsupportedMediaType';
import RangeNotSatisfiable from './RangeNotSatisfiable';
import ExpectationFailed from './ExpectationFailed';
import MisdirectedRequest from './MisdirectedRequest';
import UnprocessableEntity from './UnprocessableEntity';
import Locked from './Locked';
import FailedDependency from './FailedDependency';
import TooEarly from './TooEarly';
import UpgradeRequired from './UpgradeRequired';
import PreconditionRequired from './PreconditionRequired';
import TooManyRequests from './TooManyRequests';
import RequestHeaderFieldsTooLarge from './RequestHeaderFieldsTooLarge';
import UnavailableForLegalReasons from './UnavailableForLegalReasons';
import InternalServerError from './InternalServerError';
import NotImplemented from './NotImplemented';
import BadGateway from './BadGateway';
import ServiceUnavailable from './ServiceUnavailable';
import GatewayTimeout from './GatewayTimeout';
import HttpVersionNotSupported from './HttpVersionNotSupported';
import VariantAlsoNegotiates from './VariantAlsoNegotiates';
import UnsufficientStorage from './UnsufficientStorage';
import LoopDetected from './LoopDetected';
import NotExtended from './NotExtended';
import NetworkAuthenticationRequired from './NetworkAuthenticationRequired';
import { BaseHTTPError } from './base';
export {
BaseHTTPError,
BadRequest,
Unauthorized,
PaymentRequired,
Forbidden,
NotFound,
MethodNotAllowed,
NotAcceptable,
ProxyAuthenticationRequired,
RequestTimeout,
Conflict,
Gone,
LengthRequired,
PreconditionFailed,
PayloadTooLarge,
UriTooLong,
UnsupportedMediaType,
RangeNotSatisfiable,
ExpectationFailed,
MisdirectedRequest,
UnprocessableEntity,
Locked,
FailedDependency,
TooEarly,
UpgradeRequired,
PreconditionRequired,
TooManyRequests,
RequestHeaderFieldsTooLarge,
UnavailableForLegalReasons,
InternalServerError,
NotImplemented,
BadGateway,
ServiceUnavailable,
GatewayTimeout,
HttpVersionNotSupported,
VariantAlsoNegotiates,
UnsufficientStorage,
LoopDetected,
NotExtended,
NetworkAuthenticationRequired,
};

132
src/http/httpExceptions.ts Normal file
View File

@@ -0,0 +1,132 @@
import {
BaseHTTPError,
BadRequest,
Unauthorized,
PaymentRequired,
Forbidden,
NotFound,
MethodNotAllowed,
NotAcceptable,
ProxyAuthenticationRequired,
RequestTimeout,
Conflict,
Gone,
LengthRequired,
PreconditionFailed,
PayloadTooLarge,
UriTooLong,
UnsupportedMediaType,
RangeNotSatisfiable,
ExpectationFailed,
MisdirectedRequest,
UnprocessableEntity,
Locked,
FailedDependency,
TooEarly,
UpgradeRequired,
PreconditionRequired,
TooManyRequests,
RequestHeaderFieldsTooLarge,
UnavailableForLegalReasons,
InternalServerError,
NotImplemented,
BadGateway,
ServiceUnavailable,
GatewayTimeout,
HttpVersionNotSupported,
VariantAlsoNegotiates,
UnsufficientStorage,
LoopDetected,
NotExtended,
NetworkAuthenticationRequired,
} from './errors';
interface HttpResponseWithError {
status: number;
headers: any;
data?: any;
}
interface NumberToClass {
[key: number]: any;
}
const statusCodeToErrorFunction: NumberToClass = {
400: BadRequest,
401: Unauthorized,
402: PaymentRequired,
403: Forbidden,
404: NotFound,
405: MethodNotAllowed,
406: NotAcceptable,
407: ProxyAuthenticationRequired,
408: RequestTimeout,
409: Conflict,
410: Gone,
411: LengthRequired,
412: PreconditionFailed,
413: PayloadTooLarge,
414: UriTooLong,
415: UnsupportedMediaType,
416: RangeNotSatisfiable,
417: ExpectationFailed,
421: MisdirectedRequest,
422: UnprocessableEntity,
423: Locked,
424: FailedDependency,
425: TooEarly,
426: UpgradeRequired,
428: PreconditionRequired,
429: TooManyRequests,
431: RequestHeaderFieldsTooLarge,
451: UnavailableForLegalReasons,
500: InternalServerError,
501: NotImplemented,
502: BadGateway,
503: ServiceUnavailable,
504: GatewayTimeout,
505: HttpVersionNotSupported,
506: VariantAlsoNegotiates,
507: UnsufficientStorage,
508: LoopDetected,
510: NotExtended,
511: NetworkAuthenticationRequired,
};
/**
* @summary This function will throw an error.
*
* @param {HttpResponseWithError} response - the response from a request, must contain a status and data fields
* @throws {Error} - an http error
*/
export default function throwHttpError(response: HttpResponseWithError): never {
let error: BaseHTTPError = new BaseHTTPError(response.data);
switch (response.status) {
case 401:
error = new Unauthorized(response.data, response.headers['WWW-Authenticate']);
case 405:
// this indicates a bug in the spec if it allows a method that the server rejects
error = new MethodNotAllowed(response.data, response.headers.allowed);
case 407:
error = new ProxyAuthenticationRequired(
response.data,
response.headers['Proxy-Authenticate'],
);
case 413:
error = new PayloadTooLarge(response.data, response.headers['Retry-After']);
case 429:
error = new TooManyRequests(response.data, response.headers['Retry-After']);
case 503:
error = new ServiceUnavailable(response.data, response.headers['Retry-After']);
default:
if (response.status in statusCodeToErrorFunction) {
error = new statusCodeToErrorFunction[response.status](response.data);
} else {
const error = new BaseHTTPError(response.data);
error.statusCode = response.status;
error.title = 'unknown error';
}
}
throw error;
}

99
src/index.ts Normal file
View File

@@ -0,0 +1,99 @@
import { ActivitiesService } from './services/activities/Activities';
import { ButlerService } from './services/butler/Butler';
import { HubsService } from './services/hubs/Hubs';
import { LibraryService } from './services/library/Library';
import { LogService } from './services/log/Log';
import { MediaService } from './services/media/Media';
import { PlaylistsService } from './services/playlists/Playlists';
import { SearchService } from './services/search/Search';
import { SecurityService } from './services/security/Security';
import { ServerService } from './services/server/Server';
import { SessionsService } from './services/sessions/Sessions';
import { UpdaterService } from './services/updater/Updater';
import { VideoService } from './services/video/Video';
export * from './models';
export * as ActivitiesModels from './services/activities';
export * as ButlerModels from './services/butler';
export * as HubsModels from './services/hubs';
export * as LibraryModels from './services/library';
export * as LogModels from './services/log';
export * as PlaylistsModels from './services/playlists';
export * as SearchModels from './services/search';
export * as SecurityModels from './services/security';
export * as ServerModels from './services/server';
export * as SessionsModels from './services/sessions';
export * as UpdaterModels from './services/updater';
export * as VideoModels from './services/video';
type Config = {
apiKey?: string;
apiKeyHeader?: string;
};
export * from './http/errors';
export class PlexSDK {
public activities: ActivitiesService;
public butler: ButlerService;
public hubs: HubsService;
public library: LibraryService;
public log: LogService;
public media: MediaService;
public playlists: PlaylistsService;
public search: SearchService;
public security: SecurityService;
public server: ServerService;
public sessions: SessionsService;
public updater: UpdaterService;
public video: VideoService;
constructor({ apiKey = '', apiKeyHeader = 'X-Plex-Token' }: Config) {
this.activities = new ActivitiesService(apiKey, apiKeyHeader);
this.butler = new ButlerService(apiKey, apiKeyHeader);
this.hubs = new HubsService(apiKey, apiKeyHeader);
this.library = new LibraryService(apiKey, apiKeyHeader);
this.log = new LogService(apiKey, apiKeyHeader);
this.media = new MediaService(apiKey, apiKeyHeader);
this.playlists = new PlaylistsService(apiKey, apiKeyHeader);
this.search = new SearchService(apiKey, apiKeyHeader);
this.security = new SecurityService(apiKey, apiKeyHeader);
this.server = new ServerService(apiKey, apiKeyHeader);
this.sessions = new SessionsService(apiKey, apiKeyHeader);
this.updater = new UpdaterService(apiKey, apiKeyHeader);
this.video = new VideoService(apiKey, apiKeyHeader);
}
setBaseUrl(url: string): void {
this.activities.setBaseUrl(url);
this.butler.setBaseUrl(url);
this.hubs.setBaseUrl(url);
this.library.setBaseUrl(url);
this.log.setBaseUrl(url);
this.media.setBaseUrl(url);
this.playlists.setBaseUrl(url);
this.search.setBaseUrl(url);
this.security.setBaseUrl(url);
this.server.setBaseUrl(url);
this.sessions.setBaseUrl(url);
this.updater.setBaseUrl(url);
this.video.setBaseUrl(url);
}
setApiKey(key: string, header: string = 'X-Plex-Token') {
this.activities.setApiKey(key, header);
this.butler.setApiKey(key, header);
this.hubs.setApiKey(key, header);
this.library.setApiKey(key, header);
this.log.setApiKey(key, header);
this.media.setApiKey(key, header);
this.playlists.setApiKey(key, header);
this.search.setApiKey(key, header);
this.security.setApiKey(key, header);
this.server.setApiKey(key, header);
this.sessions.setApiKey(key, header);
this.updater.setApiKey(key, header);
this.video.setApiKey(key, header);
}
}

28
src/models.ts Normal file
View File

@@ -0,0 +1,28 @@
export type { Download } from './services/updater/models/Download';
export type { Force } from './services/playlists/models/Force';
export type { GetAvailableClientsResponse } from './services/server/models/GetAvailableClientsResponse';
export type { GetButlerTasksResponse } from './services/butler/models/GetButlerTasksResponse';
export type { GetDevicesResponse } from './services/server/models/GetDevicesResponse';
export type { GetMyPlexAccountResponse } from './services/server/models/GetMyPlexAccountResponse';
export type { GetOnDeckResponse } from './services/library/models/GetOnDeckResponse';
export type { GetRecentlyAddedResponse } from './services/library/models/GetRecentlyAddedResponse';
export type { GetSearchResultsResponse } from './services/search/models/GetSearchResultsResponse';
export type { GetServerActivitiesResponse } from './services/activities/models/GetServerActivitiesResponse';
export type { GetServerCapabilitiesResponse } from './services/server/models/GetServerCapabilitiesResponse';
export type { GetServerIdentityResponse } from './services/server/models/GetServerIdentityResponse';
export type { GetServerListResponse } from './services/server/models/GetServerListResponse';
export type { GetTranscodeSessionsResponse } from './services/sessions/models/GetTranscodeSessionsResponse';
export type { IncludeDetails } from './services/library/models/IncludeDetails';
export type { Level } from './services/log/models/Level';
export type { MinSize } from './services/server/models/MinSize';
export type { OnlyTransient } from './services/hubs/models/OnlyTransient';
export type { PlaylistType } from './services/playlists/models/PlaylistType';
export type { Scope } from './services/security/models/Scope';
export type { SecurityType } from './services/security/models/SecurityType';
export type { Skip } from './services/updater/models/Skip';
export type { Smart } from './services/playlists/models/Smart';
export type { State } from './services/video/models/State';
export type { TaskName } from './services/butler/models/TaskName';
export type { Tonight } from './services/updater/models/Tonight';
export type { Type } from './services/playlists/models/Type';
export type { Upscale } from './services/server/models/Upscale';

1977
src/services/README.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,67 @@
import BaseService from '../../BaseService';
import Response from '../../http/Response';
import { GetServerActivitiesResponse } from './models/GetServerActivitiesResponse';
import { serializePath } from '../../http/QuerySerializer';
export class ActivitiesService extends BaseService {
/**
* @summary Get Server Activities
* @description Get Server Activities
* @returns {Promise<Response<GetServerActivitiesResponse>>} - The promise with the result
*/
async getServerActivities(): Promise<Response<GetServerActivitiesResponse>> {
const urlEndpoint = '/activities';
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Cancel Server Activities
* @description Cancel Server Activities
* @param activityUUID The UUID of the activity to cancel.
* @returns {Promise<any>} - The promise with the result
*/
async cancelServerActivities(activityUuid: string): Promise<any> {
if (activityUuid === undefined) {
throw new Error(
'The following parameter is required: activityUuid, cannot be empty or blank',
);
}
let urlEndpoint = '/activities/{activityUUID}';
urlEndpoint = urlEndpoint.replace(
'{activityUUID}',
encodeURIComponent(serializePath('simple', false, activityUuid, undefined)),
);
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.delete(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
}

View File

@@ -0,0 +1 @@
export type { GetServerActivitiesResponse } from './models/GetServerActivitiesResponse';

View File

@@ -0,0 +1,19 @@
export interface GetServerActivitiesResponse {
MediaContainer?: MediaContainer;
}
interface MediaContainer {
size?: number;
Activity?: {
uuid?: string;
cancellable?: boolean;
userID?: number;
title?: string;
subtitle?: string;
progress?: number;
Context?: Context;
type_?: string;
}[];
}
interface Context {
librarySectionID?: string;
}

View File

@@ -0,0 +1,158 @@
import BaseService from '../../BaseService';
import Response from '../../http/Response';
import { GetButlerTasksResponse } from './models/GetButlerTasksResponse';
import { TaskName } from './models/TaskName';
import { serializePath } from '../../http/QuerySerializer';
export class ButlerService extends BaseService {
/**
* @summary Get Butler tasks
* @description Returns a list of butler tasks
* @returns {Promise<Response<GetButlerTasksResponse>>} - The promise with the result
*/
async getButlerTasks(): Promise<Response<GetButlerTasksResponse>> {
const urlEndpoint = '/butler';
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Start all Butler tasks
* @description This endpoint will attempt to start all Butler tasks that are enabled in the settings. Butler tasks normally run automatically during a time window configured on the server's Settings page but can be manually started using this endpoint. Tasks will run with the following criteria:
1. Any tasks not scheduled to run on the current day will be skipped.
2. If a task is configured to run at a random time during the configured window and we are outside that window, the task will start immediately.
3. If a task is configured to run at a random time during the configured window and we are within that window, the task will be scheduled at a random time within the window.
4. If we are outside the configured window, the task will start immediately.
* @returns {Promise<any>} - The promise with the result
*/
async startAllTasks(): Promise<any> {
const urlEndpoint = '/butler';
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.post(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Stop all Butler tasks
* @description This endpoint will stop all currently running tasks and remove any scheduled tasks from the queue.
* @returns {Promise<any>} - The promise with the result
*/
async stopAllTasks(): Promise<any> {
const urlEndpoint = '/butler';
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.delete(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Start a single Butler task
* @description This endpoint will attempt to start a single Butler task that is enabled in the settings. Butler tasks normally run automatically during a time window configured on the server's Settings page but can be manually started using this endpoint. Tasks will run with the following criteria:
1. Any tasks not scheduled to run on the current day will be skipped.
2. If a task is configured to run at a random time during the configured window and we are outside that window, the task will start immediately.
3. If a task is configured to run at a random time during the configured window and we are within that window, the task will be scheduled at a random time within the window.
4. If we are outside the configured window, the task will start immediately.
* @param taskName the name of the task to be started.
* @returns {Promise<any>} - The promise with the result
*/
async startTask(taskName: TaskName): Promise<any> {
if (taskName === undefined) {
throw new Error('The following parameter is required: taskName, cannot be empty or blank');
}
let urlEndpoint = '/butler/{taskName}';
urlEndpoint = urlEndpoint.replace(
'{taskName}',
encodeURIComponent(serializePath('simple', false, taskName, undefined)),
);
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.post(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Stop a single Butler task
* @description This endpoint will stop a currently running task by name, or remove it from the list of scheduled tasks if it exists. See the section above for a list of task names for this endpoint.
* @param taskName The name of the task to be started.
* @returns {Promise<any>} - The promise with the result
*/
async stopTask(taskName: TaskName): Promise<any> {
if (taskName === undefined) {
throw new Error('The following parameter is required: taskName, cannot be empty or blank');
}
let urlEndpoint = '/butler/{taskName}';
urlEndpoint = urlEndpoint.replace(
'{taskName}',
encodeURIComponent(serializePath('simple', false, taskName, undefined)),
);
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.delete(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
}

View File

@@ -0,0 +1,2 @@
export type { GetButlerTasksResponse } from './models/GetButlerTasksResponse';
export type { TaskName } from './models/TaskName';

View File

@@ -0,0 +1,13 @@
export interface GetButlerTasksResponse {
ButlerTasks?: ButlerTasks;
}
interface ButlerTasks {
ButlerTask?: {
name?: string;
interval?: number;
scheduleRandomized?: boolean;
enabled?: boolean;
title?: string;
description?: string;
}[];
}

View File

@@ -0,0 +1,15 @@
export type TaskName =
| 'BackupDatabase'
| 'BuildGracenoteCollections'
| 'CheckForUpdates'
| 'CleanOldBundles'
| 'CleanOldCacheFiles'
| 'DeepMediaAnalysis'
| 'GenerateAutoTags'
| 'GenerateChapterThumbs'
| 'GenerateMediaIndexFiles'
| 'OptimizeDatabase'
| 'RefreshLibraries'
| 'RefreshLocalMedia'
| 'RefreshPeriodicMetadata'
| 'UpgradeMediaAnalysis';

96
src/services/hubs/Hubs.ts Normal file
View File

@@ -0,0 +1,96 @@
import BaseService from '../../BaseService';
import Response from '../../http/Response';
import { OnlyTransient } from './models/OnlyTransient';
import { serializeQuery, serializePath } from '../../http/QuerySerializer';
export class HubsService extends BaseService {
/**
* @summary Get Global Hubs
* @description Get Global Hubs filtered by the parameters provided.
* @param optionalParams - Optional parameters
* @param optionalParams.count - The number of items to return with each hub.
* @param optionalParams.onlyTransient - Only return hubs which are "transient", meaning those which are prone to changing after media playback or addition (e.g. On Deck, or Recently Added).
* @returns {Promise<any>} - The promise with the result
*/
async getGlobalHubs(
optionalParams: { count?: number; onlyTransient?: OnlyTransient } = {},
): Promise<any> {
const { count, onlyTransient } = optionalParams;
const queryParams: string[] = [];
if (count) {
queryParams.push(serializeQuery('form', true, 'count', count));
}
if (onlyTransient) {
queryParams.push(serializeQuery('form', true, 'onlyTransient', onlyTransient));
}
const urlEndpoint = '/hubs';
const urlParams = queryParams.length > 0 ? `?${encodeURI(queryParams.join('&'))}` : '';
const finalUrl = `${this.baseUrl + urlEndpoint}${urlParams}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Get library specific hubs
* @description This endpoint will return a list of library specific hubs
* @param sectionId the Id of the library to query
* @param optionalParams - Optional parameters
* @param optionalParams.count - The number of items to return with each hub.
* @param optionalParams.onlyTransient - Only return hubs which are "transient", meaning those which are prone to changing after media playback or addition (e.g. On Deck, or Recently Added).
* @returns {Promise<any>} - The promise with the result
*/
async getLibraryHubs(
sectionId: number,
optionalParams: { count?: number; onlyTransient?: OnlyTransient } = {},
): Promise<any> {
const { count, onlyTransient } = optionalParams;
if (sectionId === undefined) {
throw new Error('The following parameter is required: sectionId, cannot be empty or blank');
}
const queryParams: string[] = [];
let urlEndpoint = '/hubs/sections/{sectionId}';
urlEndpoint = urlEndpoint.replace(
'{sectionId}',
encodeURIComponent(serializePath('simple', false, sectionId, undefined)),
);
if (count) {
queryParams.push(serializeQuery('form', true, 'count', count));
}
if (onlyTransient) {
queryParams.push(serializeQuery('form', true, 'onlyTransient', onlyTransient));
}
const urlParams = queryParams.length > 0 ? `?${encodeURI(queryParams.join('&'))}` : '';
const finalUrl = `${this.baseUrl + urlEndpoint}${urlParams}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
}

View File

@@ -0,0 +1 @@
export type { OnlyTransient } from './models/OnlyTransient';

View File

@@ -0,0 +1 @@
export type OnlyTransient = 0 | 1;

View File

@@ -0,0 +1,475 @@
import BaseService from '../../BaseService';
import Response from '../../http/Response';
import { GetRecentlyAddedResponse } from './models/GetRecentlyAddedResponse';
import { IncludeDetails } from './models/IncludeDetails';
import { GetOnDeckResponse } from './models/GetOnDeckResponse';
import { serializeQuery, serializePath } from '../../http/QuerySerializer';
export class LibraryService extends BaseService {
/**
* @summary Get Hash Value
* @description This resource returns hash values for local files
* @param url This is the path to the local file, must be prefixed by `file://`
* @param optionalParams - Optional parameters
* @param optionalParams.type_ - Item type
* @returns {Promise<any>} - The promise with the result
*/
async getFileHash(url: string, optionalParams: { type?: number } = {}): Promise<any> {
const { type } = optionalParams;
if (url === undefined) {
throw new Error('The following parameter is required: url, cannot be empty or blank');
}
const queryParams: string[] = [];
if (url) {
queryParams.push(serializeQuery('form', true, 'url', url));
}
if (type) {
queryParams.push(serializeQuery('form', true, 'type_', type));
}
const urlEndpoint = '/library/hashes';
const urlParams = queryParams.length > 0 ? `?${encodeURI(queryParams.join('&'))}` : '';
const finalUrl = `${this.baseUrl + urlEndpoint}${urlParams}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Get Recently Added
* @description This endpoint will return the recently added content.
* @returns {Promise<Response<GetRecentlyAddedResponse>>} - The promise with the result
*/
async getRecentlyAdded(): Promise<Response<GetRecentlyAddedResponse>> {
const urlEndpoint = '/library/recentlyAdded';
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Get All Libraries
* @description A library section (commonly referred to as just a library) is a collection of media.
Libraries are typed, and depending on their type provide either a flat or a hierarchical view of the media.
For example, a music library has an artist > albums > tracks structure, whereas a movie library is flat.
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).
* @returns {Promise<any>} - The promise with the result
*/
async getLibraries(): Promise<any> {
const urlEndpoint = '/library/sections';
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Get Library Details
* @description Returns details for the library. This can be thought of as an interstitial endpoint because it contains information about the library, rather than content itself. These details are:
- A list of `Directory` objects: These used to be used by clients to build a menuing system. There are four flavors of directory found here:
- Primary: (e.g. all, On Deck) These are still used in some clients to provide "shortcuts" to subsets of media. However, with the exception of On Deck, all of them can be created by media queries, and the desire is to allow these to be customized by users.
- Secondary: These are marked with `secondary="1"` and were used by old clients to provide nested menus allowing for primative (but structured) navigation.
- Special: There is a By Folder entry which allows browsing the media by the underlying filesystem structure, and there's a completely obsolete entry marked `search="1"` which used to be used to allow clients to build search dialogs on the fly.
- A list of `Type` objects: These represent the types of things found in this library, and for each one, a list of `Filter` and `Sort` objects. These can be used to build rich controls around a grid of media to allow filtering and organizing. Note that these filters and sorts are optional, and without them, the client won't render any filtering controls. The `Type` object contains:
- `key`: This provides the root endpoint returning the actual media list for the type.
- `type`: This is the metadata type for the type (if a standard Plex type).
- `title`: The title for for the content of this type (e.g. "Movies").
- Each `Filter` object contains a description of the filter. Note that it is not an exhaustive list of the full media query language, but an inportant subset useful for top-level API.
- `filter`: This represents the filter name used for the filter, which can be used to construct complex media queries with.
- `filterType`: This is either `string`, `integer`, or `boolean`, and describes the type of values used for the filter.
- `key`: This provides the endpoint where the possible range of values for the filter can be retrieved (e.g. for a "Genre" filter, it returns a list of all the genres in the library). This will include a `type` argument that matches the metadata type of the Type element.
- `title`: The title for the filter.
- Each `Sort` object contains a description of the sort field.
- `defaultDirection`: Can be either `asc` or `desc`, and specifies the default direction for the sort field (e.g. titles default to alphabetically ascending).
- `descKey` and `key`: Contains the parameters passed to the `sort=...` media query for each direction of the sort.
- `title`: The title of the field.
* @param sectionId the Id of the library to query
* @param optionalParams - Optional parameters
* @param optionalParams.includeDetails - Whether or not to include details for a section (types, filters, and sorts).
Only exists for backwards compatibility, media providers other than the server libraries have it on always.
* @returns {Promise<any>} - The promise with the result
*/
async getLibrary(
sectionId: number,
optionalParams: { includeDetails?: IncludeDetails } = {},
): Promise<any> {
const { includeDetails } = optionalParams;
if (sectionId === undefined) {
throw new Error('The following parameter is required: sectionId, cannot be empty or blank');
}
const queryParams: string[] = [];
let urlEndpoint = '/library/sections/{sectionId}';
urlEndpoint = urlEndpoint.replace(
'{sectionId}',
encodeURIComponent(serializePath('simple', false, sectionId, undefined)),
);
if (includeDetails) {
queryParams.push(serializeQuery('form', true, 'includeDetails', includeDetails));
}
const urlParams = queryParams.length > 0 ? `?${encodeURI(queryParams.join('&'))}` : '';
const finalUrl = `${this.baseUrl + urlEndpoint}${urlParams}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Delete Library Section
* @description Delate a library using a specific section
* @param sectionId the Id of the library to query
* @returns {Promise<any>} - The promise with the result
*/
async deleteLibrary(sectionId: number): Promise<any> {
if (sectionId === undefined) {
throw new Error('The following parameter is required: sectionId, cannot be empty or blank');
}
let urlEndpoint = '/library/sections/{sectionId}';
urlEndpoint = urlEndpoint.replace(
'{sectionId}',
encodeURIComponent(serializePath('simple', false, sectionId, undefined)),
);
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.delete(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Get Library Items
* @description This endpoint will return a list of library items filtered by the filter and type provided
* @param sectionId the Id of the library to query
* @param optionalParams - Optional parameters
* @param optionalParams.type_ - item type
* @param optionalParams.filter - the filter parameter
* @returns {Promise<any>} - The promise with the result
*/
async getLibraryItems(
sectionId: number,
optionalParams: { type?: number; filter?: string } = {},
): Promise<any> {
const { type, filter } = optionalParams;
if (sectionId === undefined) {
throw new Error('The following parameter is required: sectionId, cannot be empty or blank');
}
const queryParams: string[] = [];
let urlEndpoint = '/library/sections/{sectionId}/all';
urlEndpoint = urlEndpoint.replace(
'{sectionId}',
encodeURIComponent(serializePath('simple', false, sectionId, undefined)),
);
if (type) {
queryParams.push(serializeQuery('form', true, 'type_', type));
}
if (filter) {
queryParams.push(serializeQuery('form', true, 'filter', filter));
}
const urlParams = queryParams.length > 0 ? `?${encodeURI(queryParams.join('&'))}` : '';
const finalUrl = `${this.baseUrl + urlEndpoint}${urlParams}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Refresh Library
* @description This endpoint Refreshes the library.
* @param sectionId the Id of the library to refresh
* @returns {Promise<any>} - The promise with the result
*/
async refreshLibrary(sectionId: number): Promise<any> {
if (sectionId === undefined) {
throw new Error('The following parameter is required: sectionId, cannot be empty or blank');
}
let urlEndpoint = '/library/sections/{sectionId}/refresh';
urlEndpoint = urlEndpoint.replace(
'{sectionId}',
encodeURIComponent(serializePath('simple', false, sectionId, undefined)),
);
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Get Latest Library Items
* @description This endpoint will return a list of the latest library items filtered by the filter and type provided
* @param sectionId the Id of the library to query
* @param type_ item type
* @param optionalParams - Optional parameters
* @param optionalParams.filter - the filter parameter
* @returns {Promise<any>} - The promise with the result
*/
async getLatestLibraryItems(
sectionId: number,
type: number,
optionalParams: { filter?: string } = {},
): Promise<any> {
const { filter } = optionalParams;
if (sectionId === undefined || type === undefined) {
throw new Error(
'The following are required parameters: sectionId,type, cannot be empty or blank',
);
}
const queryParams: string[] = [];
let urlEndpoint = '/library/sections/{sectionId}/latest';
urlEndpoint = urlEndpoint.replace(
'{sectionId}',
encodeURIComponent(serializePath('simple', false, sectionId, undefined)),
);
if (type) {
queryParams.push(serializeQuery('form', true, 'type_', type));
}
if (filter) {
queryParams.push(serializeQuery('form', true, 'filter', filter));
}
const urlParams = queryParams.length > 0 ? `?${encodeURI(queryParams.join('&'))}` : '';
const finalUrl = `${this.baseUrl + urlEndpoint}${urlParams}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Get Common Library Items
* @description Represents a "Common" item. It contains only the common attributes of the items selected by the provided filter
* @param sectionId the Id of the library to query
* @param type_ item type
* @param optionalParams - Optional parameters
* @param optionalParams.filter - the filter parameter
* @returns {Promise<any>} - The promise with the result
*/
async getCommonLibraryItems(
sectionId: number,
type: number,
optionalParams: { filter?: string } = {},
): Promise<any> {
const { filter } = optionalParams;
if (sectionId === undefined || type === undefined) {
throw new Error(
'The following are required parameters: sectionId,type, cannot be empty or blank',
);
}
const queryParams: string[] = [];
let urlEndpoint = '/library/sections/{sectionId}/common';
urlEndpoint = urlEndpoint.replace(
'{sectionId}',
encodeURIComponent(serializePath('simple', false, sectionId, undefined)),
);
if (type) {
queryParams.push(serializeQuery('form', true, 'type_', type));
}
if (filter) {
queryParams.push(serializeQuery('form', true, 'filter', filter));
}
const urlParams = queryParams.length > 0 ? `?${encodeURI(queryParams.join('&'))}` : '';
const finalUrl = `${this.baseUrl + urlEndpoint}${urlParams}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Get Items Metadata
* @description This endpoint will return the metadata of a library item specified with the ratingKey.
* @param ratingKey the id of the library item to return the children of.
* @returns {Promise<any>} - The promise with the result
*/
async getMetadata(ratingKey: number): Promise<any> {
if (ratingKey === undefined) {
throw new Error('The following parameter is required: ratingKey, cannot be empty or blank');
}
let urlEndpoint = '/library/metadata/{ratingKey}';
urlEndpoint = urlEndpoint.replace(
'{ratingKey}',
encodeURIComponent(serializePath('simple', false, ratingKey, undefined)),
);
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Get Items Children
* @description This endpoint will return the children of of a library item specified with the ratingKey.
* @param ratingKey the id of the library item to return the children of.
* @returns {Promise<any>} - The promise with the result
*/
async getMetadataChildren(ratingKey: number): Promise<any> {
if (ratingKey === undefined) {
throw new Error('The following parameter is required: ratingKey, cannot be empty or blank');
}
let urlEndpoint = '/library/metadata/{ratingKey}/children';
urlEndpoint = urlEndpoint.replace(
'{ratingKey}',
encodeURIComponent(serializePath('simple', false, ratingKey, undefined)),
);
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Get On Deck
* @description This endpoint will return the on deck content.
* @returns {Promise<Response<GetOnDeckResponse>>} - The promise with the result
*/
async getOnDeck(): Promise<Response<GetOnDeckResponse>> {
const urlEndpoint = '/library/onDeck';
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
}

View File

@@ -0,0 +1,3 @@
export type { GetOnDeckResponse } from './models/GetOnDeckResponse';
export type { GetRecentlyAddedResponse } from './models/GetRecentlyAddedResponse';
export type { IncludeDetails } from './models/IncludeDetails';

View File

@@ -0,0 +1,101 @@
export interface GetOnDeckResponse {
MediaContainer?: MediaContainer;
}
interface MediaContainer {
size?: number;
allowSync?: boolean;
identifier?: string;
mediaTagPrefix?: string;
mediaTagVersion?: number;
mixedParents?: boolean;
Metadata?: {
allowSync?: boolean;
librarySectionID?: number;
librarySectionTitle?: string;
librarySectionUUID?: string;
ratingKey?: number;
key?: string;
parentRatingKey?: number;
grandparentRatingKey?: number;
guid?: string;
parentGuid?: string;
grandparentGuid?: string;
title?: string;
grandparentKey?: string;
parentKey?: string;
librarySectionKey?: string;
grandparentTitle?: string;
parentTitle?: string;
contentRating?: string;
summary?: string;
index?: number;
parentIndex?: number;
lastViewedAt?: number;
year?: number;
thumb?: string;
art?: string;
parentThumb?: string;
grandparentThumb?: string;
grandparentArt?: string;
grandparentTheme?: string;
duration?: number;
originallyAvailableAt?: string;
addedAt?: number;
updatedAt?: number;
Media?: {
id?: number;
duration?: number;
bitrate?: number;
width?: number;
height?: number;
aspectRatio?: number;
audioChannels?: number;
audioCodec?: string;
videoCodec?: string;
videoResolution?: string;
container?: string;
videoFrameRate?: string;
audioProfile?: string;
videoProfile?: string;
Part?: {
id?: number;
key?: string;
duration?: number;
file?: string;
size?: number;
audioProfile?: string;
container?: string;
videoProfile?: string;
Stream?: {
id?: number;
streamType?: number;
codec?: string;
index?: number;
bitrate?: number;
language?: string;
languageTag?: string;
languageCode?: string;
bitDepth?: number;
chromaLocation?: string;
chromaSubsampling?: string;
codedHeight?: number;
codedWidth?: number;
colorRange?: string;
frameRate?: number;
height?: number;
level?: number;
profile?: string;
refFrames?: number;
width?: number;
displayTitle?: string;
extendedDisplayTitle?: string;
default_?: boolean;
}[];
}[];
}[];
Guid?: {
id?: string;
}[];
type_?: string;
}[];
}

View File

@@ -0,0 +1,83 @@
export interface GetRecentlyAddedResponse {
MediaContainer?: MediaContainer;
}
interface MediaContainer {
size?: number;
allowSync?: boolean;
identifier?: string;
mediaTagPrefix?: string;
mediaTagVersion?: number;
mixedParents?: boolean;
Metadata?: {
allowSync?: boolean;
librarySectionID?: number;
librarySectionTitle?: string;
librarySectionUUID?: string;
ratingKey?: number;
key?: string;
guid?: string;
studio?: string;
title?: string;
contentRating?: string;
summary?: string;
rating?: number;
audienceRating?: number;
year?: number;
tagline?: string;
thumb?: string;
art?: string;
duration?: number;
originallyAvailableAt?: string;
addedAt?: number;
updatedAt?: number;
audienceRatingImage?: string;
chapterSource?: string;
primaryExtraKey?: string;
ratingImage?: string;
Media?: {
id?: number;
duration?: number;
bitrate?: number;
width?: number;
height?: number;
aspectRatio?: number;
audioChannels?: number;
audioCodec?: string;
videoCodec?: string;
videoResolution?: number;
container?: string;
videoFrameRate?: string;
optimizedForStreaming?: number;
has64bitOffsets?: boolean;
videoProfile?: string;
Part?: {
id?: number;
key?: string;
duration?: number;
file?: string;
size?: number;
container?: string;
has64bitOffsets?: boolean;
hasThumbnail?: number;
optimizedForStreaming?: boolean;
videoProfile?: string;
}[];
}[];
Genre?: {
tag?: string;
}[];
Director?: {
tag?: string;
}[];
Writer?: {
tag?: string;
}[];
Country?: {
tag?: string;
}[];
Role?: {
tag?: string;
}[];
type_?: string;
}[];
}

View File

@@ -0,0 +1 @@
export type IncludeDetails = 0 | 1;

108
src/services/log/Log.ts Normal file
View File

@@ -0,0 +1,108 @@
import BaseService from '../../BaseService';
import Response from '../../http/Response';
import { Level } from './models/Level';
import { serializeQuery } from '../../http/QuerySerializer';
export class LogService extends BaseService {
/**
* @summary Logging a single line message.
* @description This endpoint will write a single-line log message, including a level and source to the main Plex Media Server log.
* @param level An integer log level to write to the PMS log with.
0: Error
1: Warning
2: Info
3: Debug
4: Verbose
* @param message The text of the message to write to the log.
* @param source a string indicating the source of the message.
* @returns {Promise<any>} - The promise with the result
*/
async logLine(level: Level, message: string, source: string): Promise<any> {
if (level === undefined || message === undefined || source === undefined) {
throw new Error(
'The following are required parameters: level,message,source, cannot be empty or blank',
);
}
const queryParams: string[] = [];
if (level) {
queryParams.push(serializeQuery('form', true, 'level', level));
}
if (message) {
queryParams.push(serializeQuery('form', true, 'message', message));
}
if (source) {
queryParams.push(serializeQuery('form', true, 'source', source));
}
const urlEndpoint = '/log';
const finalUrl = `${this.baseUrl + urlEndpoint}?${encodeURI(queryParams.join('&'))}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Logging a multi-line message
* @description This endpoint will write multiple lines to the main Plex Media Server log in a single request. It takes a set of query strings as would normally sent to the above GET endpoint as a linefeed-separated block of POST data. The parameters for each query string match as above.
* @returns {Promise<any>} - The promise with the result
*/
async logMultiLine(): Promise<any> {
const urlEndpoint = '/log';
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.post(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Enabling Papertrail
* @description This endpoint will enable all Plex Media Serverlogs to be sent to the Papertrail networked logging site for a period of time.
* @returns {Promise<any>} - The promise with the result
*/
async enablePaperTrail(): Promise<any> {
const urlEndpoint = '/log/networked';
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
}

View File

@@ -0,0 +1 @@
export type { Level } from './models/Level';

View File

@@ -0,0 +1 @@
export type Level = 0 | 1 | 2 | 3 | 4;

114
src/services/media/Media.ts Normal file
View File

@@ -0,0 +1,114 @@
import BaseService from '../../BaseService';
import Response from '../../http/Response';
import { serializeQuery } from '../../http/QuerySerializer';
export class MediaService extends BaseService {
/**
* @summary Mark Media Played
* @description This will mark the provided media key as Played.
* @param key The media key to mark as played
* @returns {Promise<any>} - The promise with the result
*/
async markPlayed(key: number): Promise<any> {
if (key === undefined) {
throw new Error('The following parameter is required: key, cannot be empty or blank');
}
const queryParams: string[] = [];
if (key) {
queryParams.push(serializeQuery('form', true, 'key', key));
}
const urlEndpoint = '/:/scrobble';
const finalUrl = `${this.baseUrl + urlEndpoint}?${encodeURI(queryParams.join('&'))}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Mark Media Unplayed
* @description This will mark the provided media key as Unplayed.
* @param key The media key to mark as Unplayed
* @returns {Promise<any>} - The promise with the result
*/
async markUnplayed(key: number): Promise<any> {
if (key === undefined) {
throw new Error('The following parameter is required: key, cannot be empty or blank');
}
const queryParams: string[] = [];
if (key) {
queryParams.push(serializeQuery('form', true, 'key', key));
}
const urlEndpoint = '/:/unscrobble';
const finalUrl = `${this.baseUrl + urlEndpoint}?${encodeURI(queryParams.join('&'))}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Update Media Play Progress
* @description This API command can be used to update the play progress of a media item.
* @param key the media key
* @param time The time, in milliseconds, used to set the media playback progress.
* @param state The playback state of the media item.
* @returns {Promise<any>} - The promise with the result
*/
async updatePlayProgress(key: string, time: number, state: string): Promise<any> {
if (key === undefined || time === undefined || state === undefined) {
throw new Error(
'The following are required parameters: key,time,state, cannot be empty or blank',
);
}
const queryParams: string[] = [];
if (key) {
queryParams.push(serializeQuery('form', true, 'key', key));
}
if (time) {
queryParams.push(serializeQuery('form', true, 'time', time));
}
if (state) {
queryParams.push(serializeQuery('form', true, 'state', state));
}
const urlEndpoint = '/:/progress';
const finalUrl = `${this.baseUrl + urlEndpoint}?${encodeURI(queryParams.join('&'))}`;
const response: any = await this.httpClient.post(
finalUrl,
{ key, time, state },
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
}

View File

@@ -0,0 +1,380 @@
import BaseService from '../../BaseService';
import Response from '../../http/Response';
import { Type } from './models/Type';
import { Smart } from './models/Smart';
import { PlaylistType } from './models/PlaylistType';
import { Force } from './models/Force';
import { serializeQuery, serializePath } from '../../http/QuerySerializer';
export class PlaylistsService extends BaseService {
/**
* @summary Create a Playlist
* @description Create a new playlist. By default the playlist is blank. To create a playlist along with a first item, pass:
- `uri` - The content URI for what we're playing (e.g. `library://...`).
- `playQueueID` - To create a playlist from an existing play queue.
* @param title name of the playlist
* @param type_ type of playlist to create
* @param smart whether the playlist is smart or not
* @param optionalParams - Optional parameters
* @param optionalParams.uri - the content URI for the playlist
* @param optionalParams.playQueueID - the play queue to copy to a playlist
* @returns {Promise<any>} - The promise with the result
*/
async createPlaylist(
title: string,
type: Type,
smart: Smart,
optionalParams: { uri?: string; playQueueId?: number } = {},
): Promise<any> {
const { uri, playQueueId } = optionalParams;
if (title === undefined || type === undefined || smart === undefined) {
throw new Error(
'The following are required parameters: title,type,smart, cannot be empty or blank',
);
}
const queryParams: string[] = [];
if (title) {
queryParams.push(serializeQuery('form', true, 'title', title));
}
if (type) {
queryParams.push(serializeQuery('form', true, 'type_', type));
}
if (smart) {
queryParams.push(serializeQuery('form', true, 'smart', smart));
}
if (uri) {
queryParams.push(serializeQuery('form', true, 'uri', uri));
}
if (playQueueId) {
queryParams.push(serializeQuery('form', true, 'playQueueID', playQueueId));
}
const urlEndpoint = '/playlists';
const urlParams = queryParams.length > 0 ? `?${encodeURI(queryParams.join('&'))}` : '';
const finalUrl = `${this.baseUrl + urlEndpoint}${urlParams}`;
const response: any = await this.httpClient.post(
finalUrl,
{ title, type_: type, smart, uri, playQueueID: playQueueId },
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Get All Playlists
* @description Get All Playlists given the specified filters.
* @param optionalParams - Optional parameters
* @param optionalParams.playlistType - limit to a type of playlist.
* @param optionalParams.smart - type of playlists to return (default is all).
* @returns {Promise<any>} - The promise with the result
*/
async getPlaylists(
optionalParams: { playlistType?: PlaylistType; smart?: Smart } = {},
): Promise<any> {
const { playlistType, smart } = optionalParams;
const queryParams: string[] = [];
if (playlistType) {
queryParams.push(serializeQuery('form', true, 'playlistType', playlistType));
}
if (smart) {
queryParams.push(serializeQuery('form', true, 'smart', smart));
}
const urlEndpoint = '/playlists/all';
const urlParams = queryParams.length > 0 ? `?${encodeURI(queryParams.join('&'))}` : '';
const finalUrl = `${this.baseUrl + urlEndpoint}${urlParams}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Retrieve Playlist
* @description Gets detailed metadata for a playlist. A playlist for many purposes (rating, editing metadata, tagging), can be treated like a regular metadata item:
Smart playlist details contain the `content` attribute. This is the content URI for the generator. This can then be parsed by a client to provide smart playlist editing.
* @param playlistID the ID of the playlist
* @returns {Promise<any>} - The promise with the result
*/
async getPlaylist(playlistId: number): Promise<any> {
if (playlistId === undefined) {
throw new Error('The following parameter is required: playlistId, cannot be empty or blank');
}
let urlEndpoint = '/playlists/{playlistID}';
urlEndpoint = urlEndpoint.replace(
'{playlistID}',
encodeURIComponent(serializePath('simple', false, playlistId, undefined)),
);
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Update a Playlist
* @description From PMS version 1.9.1 clients can also edit playlist metadata using this endpoint as they would via `PUT /library/metadata/{playlistID}`
* @param playlistID the ID of the playlist
* @returns {Promise<any>} - The promise with the result
*/
async updatePlaylist(playlistId: number): Promise<any> {
if (playlistId === undefined) {
throw new Error('The following parameter is required: playlistId, cannot be empty or blank');
}
let urlEndpoint = '/playlists/{playlistID}';
urlEndpoint = urlEndpoint.replace(
'{playlistID}',
encodeURIComponent(serializePath('simple', false, playlistId, undefined)),
);
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.put(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Deletes a Playlist
* @description This endpoint will delete a playlist
* @param playlistID the ID of the playlist
* @returns {Promise<any>} - The promise with the result
*/
async deletePlaylist(playlistId: number): Promise<any> {
if (playlistId === undefined) {
throw new Error('The following parameter is required: playlistId, cannot be empty or blank');
}
let urlEndpoint = '/playlists/{playlistID}';
urlEndpoint = urlEndpoint.replace(
'{playlistID}',
encodeURIComponent(serializePath('simple', false, playlistId, undefined)),
);
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.delete(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Retrieve Playlist Contents
* @description Gets the contents of a playlist. Should be paged by clients via standard mechanisms.
By default leaves are returned (e.g. episodes, movies). In order to return other types you can use the `type` parameter.
For example, you could use this to display a list of recently added albums vis a smart playlist.
Note that for dumb playlists, items have a `playlistItemID` attribute which is used for deleting or moving items.
* @param playlistID the ID of the playlist
* @param type_ the metadata type of the item to return
* @returns {Promise<any>} - The promise with the result
*/
async getPlaylistContents(playlistId: number, type: number): Promise<any> {
if (playlistId === undefined || type === undefined) {
throw new Error(
'The following are required parameters: playlistId,type, cannot be empty or blank',
);
}
const queryParams: string[] = [];
let urlEndpoint = '/playlists/{playlistID}/items';
urlEndpoint = urlEndpoint.replace(
'{playlistID}',
encodeURIComponent(serializePath('simple', false, playlistId, undefined)),
);
if (type) {
queryParams.push(serializeQuery('form', true, 'type_', type));
}
const finalUrl = `${this.baseUrl + urlEndpoint}?${encodeURI(queryParams.join('&'))}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Adding to a Playlist
* @description Adds a generator to a playlist, same parameters as the POST above. With a dumb playlist, this adds the specified items to the playlist.
With a smart playlist, passing a new `uri` parameter replaces the rules for the playlist. Returns the playlist.
* @param playlistID the ID of the playlist
* @param uri the content URI for the playlist
* @param playQueueID the play queue to add to a playlist
* @returns {Promise<any>} - The promise with the result
*/
async addPlaylistContents(playlistId: number, uri: string, playQueueId: number): Promise<any> {
if (playlistId === undefined || uri === undefined || playQueueId === undefined) {
throw new Error(
'The following are required parameters: playlistId,uri,playQueueId, cannot be empty or blank',
);
}
const queryParams: string[] = [];
let urlEndpoint = '/playlists/{playlistID}/items';
urlEndpoint = urlEndpoint.replace(
'{playlistID}',
encodeURIComponent(serializePath('simple', false, playlistId, undefined)),
);
if (uri) {
queryParams.push(serializeQuery('form', true, 'uri', uri));
}
if (playQueueId) {
queryParams.push(serializeQuery('form', true, 'playQueueID', playQueueId));
}
const finalUrl = `${this.baseUrl + urlEndpoint}?${encodeURI(queryParams.join('&'))}`;
const response: any = await this.httpClient.put(
finalUrl,
{ uri, playQueueID: playQueueId },
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Delete Playlist Contents
* @description Clears a playlist, only works with dumb playlists. Returns the playlist.
* @param playlistID the ID of the playlist
* @returns {Promise<any>} - The promise with the result
*/
async clearPlaylistContents(playlistId: number): Promise<any> {
if (playlistId === undefined) {
throw new Error('The following parameter is required: playlistId, cannot be empty or blank');
}
let urlEndpoint = '/playlists/{playlistID}/items';
urlEndpoint = urlEndpoint.replace(
'{playlistID}',
encodeURIComponent(serializePath('simple', false, playlistId, undefined)),
);
const finalUrl = `${this.baseUrl + urlEndpoint}`;
const response: any = await this.httpClient.delete(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Upload Playlist
* @description Imports m3u playlists by passing a path on the server to scan for m3u-formatted playlist files, or a path to a single playlist file.
* @param path absolute path to a directory on the server where m3u files are stored, or the absolute path to a playlist file on the server.
If the `path` argument is a directory, that path will be scanned for playlist files to be processed.
Each file in that directory creates a separate playlist, with a name based on the filename of the file that created it.
The GUID of each playlist is based on the filename.
If the `path` argument is a file, that file will be used to create a new playlist, with the name based on the filename of the file that created it.
The GUID of each playlist is based on the filename.
* @param force force overwriting of duplicate playlists. By default, a playlist file uploaded with the same path will overwrite the existing playlist.
The `force` argument is used to disable overwriting. If the `force` argument is set to 0, a new playlist will be created suffixed with the date and time that the duplicate was uploaded.
* @returns {Promise<any>} - The promise with the result
*/
async uploadPlaylist(path: string, force: Force): Promise<any> {
if (path === undefined || force === undefined) {
throw new Error(
'The following are required parameters: path,force, cannot be empty or blank',
);
}
const queryParams: string[] = [];
if (path) {
queryParams.push(serializeQuery('form', true, 'path', path));
}
if (force) {
queryParams.push(serializeQuery('form', true, 'force', force));
}
const urlEndpoint = '/playlists/upload';
const finalUrl = `${this.baseUrl + urlEndpoint}?${encodeURI(queryParams.join('&'))}`;
const response: any = await this.httpClient.post(
finalUrl,
{ path, force },
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
}

View File

@@ -0,0 +1,4 @@
export type { Force } from './models/Force';
export type { PlaylistType } from './models/PlaylistType';
export type { Smart } from './models/Smart';
export type { Type } from './models/Type';

View File

@@ -0,0 +1 @@
export type Force = 0 | 1;

View File

@@ -0,0 +1 @@
export type PlaylistType = 'audio' | 'video' | 'photo';

View File

@@ -0,0 +1 @@
export type Smart = 0 | 1;

View File

@@ -0,0 +1 @@
export type Type = 'audio' | 'video' | 'photo';

View File

@@ -0,0 +1,149 @@
import BaseService from '../../BaseService';
import Response from '../../http/Response';
import { GetSearchResultsResponse } from './models/GetSearchResultsResponse';
import { serializeQuery } from '../../http/QuerySerializer';
export class SearchService extends BaseService {
/**
* @summary Perform a search
* @description This endpoint performs a search across all library sections, or a single section, and returns matches as hubs, split up by type. It performs spell checking, looks for partial matches, and orders the hubs based on quality of results. In addition, based on matches, it will return other related matches (e.g. for a genre match, it may return movies in that genre, or for an actor match, movies with that actor).
In the response's items, the following extra attributes are returned to further describe or disambiguate the result:
- `reason`: The reason for the result, if not because of a direct search term match; can be either:
- `section`: There are multiple identical results from different sections.
- `originalTitle`: There was a search term match from the original title field (sometimes those can be very different or in a foreign language).
- `<hub identifier>`: If the reason for the result is due to a result in another hub, the source hub identifier is returned. For example, if the search is for "dylan" then Bob Dylan may be returned as an artist result, an a few of his albums returned as album results with a reason code of `artist` (the identifier of that particular hub). Or if the search is for "arnold", there might be movie results returned with a reason of `actor`
- `reasonTitle`: The string associated with the reason code. For a section reason, it'll be the section name; For a hub identifier, it'll be a string associated with the match (e.g. `Arnold Schwarzenegger` for movies which were returned because the search was for "arnold").
- `reasonID`: The ID of the item associated with the reason for the result. This might be a section ID, a tag ID, an artist ID, or a show ID.
This request is intended to be very fast, and called as the user types.
* @param query The query term
* @param optionalParams - Optional parameters
* @param optionalParams.sectionId - This gives context to the search, and can result in re-ordering of search result hubs
* @param optionalParams.limit - The number of items to return per hub
* @returns {Promise<any>} - The promise with the result
*/
async performSearch(
query: string,
optionalParams: { sectionId?: number; limit?: number } = {},
): Promise<any> {
const { sectionId, limit } = optionalParams;
if (query === undefined) {
throw new Error('The following parameter is required: query, cannot be empty or blank');
}
const queryParams: string[] = [];
if (query) {
queryParams.push(serializeQuery('form', true, 'query', query));
}
if (sectionId) {
queryParams.push(serializeQuery('form', true, 'sectionId', sectionId));
}
if (limit) {
queryParams.push(serializeQuery('form', true, 'limit', limit));
}
const urlEndpoint = '/hubs/search';
const urlParams = queryParams.length > 0 ? `?${encodeURI(queryParams.join('&'))}` : '';
const finalUrl = `${this.baseUrl + urlEndpoint}${urlParams}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Perform a voice search
* @description This endpoint performs a search specifically tailored towards voice or other imprecise input which may work badly with the substring and spell-checking heuristics used by the `/hubs/search` endpoint.
It uses a [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance) heuristic to search titles, and as such is much slower than the other search endpoint.
Whenever possible, clients should limit the search to the appropriate type.
Results, as well as their containing per-type hubs, contain a `distance` attribute which can be used to judge result quality.
* @param query The query term
* @param optionalParams - Optional parameters
* @param optionalParams.sectionId - This gives context to the search, and can result in re-ordering of search result hubs
* @param optionalParams.limit - The number of items to return per hub
* @returns {Promise<any>} - The promise with the result
*/
async performVoiceSearch(
query: string,
optionalParams: { sectionId?: number; limit?: number } = {},
): Promise<any> {
const { sectionId, limit } = optionalParams;
if (query === undefined) {
throw new Error('The following parameter is required: query, cannot be empty or blank');
}
const queryParams: string[] = [];
if (query) {
queryParams.push(serializeQuery('form', true, 'query', query));
}
if (sectionId) {
queryParams.push(serializeQuery('form', true, 'sectionId', sectionId));
}
if (limit) {
queryParams.push(serializeQuery('form', true, 'limit', limit));
}
const urlEndpoint = '/hubs/search/voice';
const urlParams = queryParams.length > 0 ? `?${encodeURI(queryParams.join('&'))}` : '';
const finalUrl = `${this.baseUrl + urlEndpoint}${urlParams}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Get Search Results
* @description This will search the database for the string provided.
* @param query The search query string to use
* @returns {Promise<Response<GetSearchResultsResponse>>} - The promise with the result
*/
async getSearchResults(query: string): Promise<Response<GetSearchResultsResponse>> {
if (query === undefined) {
throw new Error('The following parameter is required: query, cannot be empty or blank');
}
const queryParams: string[] = [];
if (query) {
queryParams.push(serializeQuery('form', true, 'query', query));
}
const urlEndpoint = '/search';
const finalUrl = `${this.baseUrl + urlEndpoint}?${encodeURI(queryParams.join('&'))}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
}

View File

@@ -0,0 +1 @@
export type { GetSearchResultsResponse } from './models/GetSearchResultsResponse';

View File

@@ -0,0 +1,85 @@
export interface GetSearchResultsResponse {
MediaContainer?: MediaContainer;
}
interface MediaContainer {
size?: number;
identifier?: string;
mediaTagPrefix?: string;
mediaTagVersion?: number;
Metadata?: {
allowSync?: boolean;
librarySectionID?: number;
librarySectionTitle?: string;
librarySectionUUID?: string;
personal?: boolean;
sourceTitle?: string;
ratingKey?: number;
key?: string;
guid?: string;
studio?: string;
title?: string;
contentRating?: string;
summary?: string;
rating?: number;
audienceRating?: number;
year?: number;
tagline?: string;
thumb?: string;
art?: string;
duration?: number;
originallyAvailableAt?: string;
addedAt?: number;
updatedAt?: number;
audienceRatingImage?: string;
chapterSource?: string;
primaryExtraKey?: string;
ratingImage?: string;
Media?: {
id?: number;
duration?: number;
bitrate?: number;
width?: number;
height?: number;
aspectRatio?: number;
audioChannels?: number;
audioCodec?: string;
videoCodec?: string;
videoResolution?: number;
container?: string;
videoFrameRate?: string;
audioProfile?: string;
videoProfile?: string;
Part?: {
id?: number;
key?: string;
duration?: number;
file?: string;
size?: number;
audioProfile?: string;
container?: string;
videoProfile?: string;
}[];
}[];
Genre?: {
tag?: string;
}[];
Director?: {
tag?: string;
}[];
Writer?: {
tag?: string;
}[];
Country?: {
tag?: string;
}[];
Role?: {
tag?: string;
}[];
type_?: string;
}[];
Provider?: {
key?: string;
title?: string;
type_?: string;
}[];
}

View File

@@ -0,0 +1,83 @@
import BaseService from '../../BaseService';
import Response from '../../http/Response';
import { SecurityType } from './models/SecurityType';
import { Scope } from './models/Scope';
import { serializeQuery } from '../../http/QuerySerializer';
export class SecurityService extends BaseService {
/**
* @summary Get a Transient Token.
* @description This endpoint provides the caller with a temporary token with the same access level as the caller's token. These tokens are valid for up to 48 hours and are destroyed if the server instance is restarted.
* @param type_ `delegation` - This is the only supported `type` parameter.
* @param scope `all` - This is the only supported `scope` parameter.
* @returns {Promise<any>} - The promise with the result
*/
async getTransientToken(type: SecurityType, scope: Scope): Promise<any> {
if (type === undefined || scope === undefined) {
throw new Error(
'The following are required parameters: type,scope, cannot be empty or blank',
);
}
const queryParams: string[] = [];
if (type) {
queryParams.push(serializeQuery('form', true, 'type_', type));
}
if (scope) {
queryParams.push(serializeQuery('form', true, 'scope', scope));
}
const urlEndpoint = '/security/token';
const finalUrl = `${this.baseUrl + urlEndpoint}?${encodeURI(queryParams.join('&'))}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
/**
* @summary Get Source Connection Information
* @description If a caller requires connection details and a transient token for a source that is known to the server, for example a cloud media provider or shared PMS, then this endpoint can be called. This endpoint is only accessible with either an admin token or a valid transient token generated from an admin token.
Note: requires Plex Media Server >= 1.15.4.
* @param source The source identifier with an included prefix.
* @returns {Promise<any>} - The promise with the result
*/
async getSourceConnectionInformation(source: string): Promise<any> {
if (source === undefined) {
throw new Error('The following parameter is required: source, cannot be empty or blank');
}
const queryParams: string[] = [];
if (source) {
queryParams.push(serializeQuery('form', true, 'source', source));
}
const urlEndpoint = '/security/resources';
const finalUrl = `${this.baseUrl + urlEndpoint}?${encodeURI(queryParams.join('&'))}`;
const response: any = await this.httpClient.get(
finalUrl,
{},
{
...this.getAuthorizationHeader(),
},
true,
);
const responseModel = {
data: response.data,
headers: response.headers,
};
return responseModel;
}
}

Some files were not shown because too many files have changed in this diff Show More