diff --git a/.speakeasy/gen.lock b/.speakeasy/gen.lock index b37b1ab6..2f1ad2fd 100755 --- a/.speakeasy/gen.lock +++ b/.speakeasy/gen.lock @@ -1,19 +1,19 @@ lockVersion: 2.0.0 id: 1732900d-e173-47c1-a90d-d45182eb35d9 management: - docChecksum: 184db864cffee563d43aae06915b9671 + docChecksum: fa4c9c5c23680ad02fdbe831ad9d2403 docVersion: 0.0.3 - speakeasyVersion: 1.529.1 - generationVersion: 2.566.5 - releaseVersion: 0.14.2 - configChecksum: bad4d944912dc794f8497b0cfca8b8b2 + speakeasyVersion: 1.531.4 + generationVersion: 2.570.4 + releaseVersion: 0.15.0 + configChecksum: 8a775baa0e7c2ba4c10a7c8bb26dbcd3 repoURL: https://github.com/LukeHagar/plexjava.git published: true features: java: additionalDependencies: 0.1.0 constsAndDefaults: 0.1.1 - core: 3.36.2 + core: 3.37.1 deprecations: 2.81.1 downloadStreams: 0.1.1 enums: 2.81.2 diff --git a/.speakeasy/gen.yaml b/.speakeasy/gen.yaml index 17697f2e..c497de37 100644 --- a/.speakeasy/gen.yaml +++ b/.speakeasy/gen.yaml @@ -15,7 +15,7 @@ generation: oAuth2ClientCredentialsEnabled: true oAuth2PasswordEnabled: false java: - version: 0.14.2 + version: 0.15.0 additionalDependencies: [] additionalPlugins: [] artifactID: plexapi diff --git a/.speakeasy/workflow.lock b/.speakeasy/workflow.lock index 4aafd451..8eba3ea2 100644 --- a/.speakeasy/workflow.lock +++ b/.speakeasy/workflow.lock @@ -1,4 +1,4 @@ -speakeasyVersion: 1.529.1 +speakeasyVersion: 1.531.4 sources: my-source: sourceNamespace: my-source @@ -8,19 +8,19 @@ sources: - latest plexapi: sourceNamespace: plexapi - sourceRevisionDigest: sha256:ea508e3da23b283f47aeb4384d6b3ab374a27bdffb8d8d8376cedbad3ad77a06 - sourceBlobDigest: sha256:c210adbd02b0213b9e6f95ffc27f60ec38f8f9040137d55ccf2b650d92ab0ff5 + sourceRevisionDigest: sha256:db75a06885b897418dcb580679b5f744348573ad317155172346d32442420e5e + sourceBlobDigest: sha256:ac0a3c3a6df002378b3522325c2330ba4309fbbbfa503a04b8267398f20033f5 tags: - latest - - speakeasy-sdk-regen-1743899436 + - speakeasy-sdk-regen-1744590653 targets: plexjava: source: plexapi sourceNamespace: plexapi - sourceRevisionDigest: sha256:ea508e3da23b283f47aeb4384d6b3ab374a27bdffb8d8d8376cedbad3ad77a06 - sourceBlobDigest: sha256:c210adbd02b0213b9e6f95ffc27f60ec38f8f9040137d55ccf2b650d92ab0ff5 + sourceRevisionDigest: sha256:db75a06885b897418dcb580679b5f744348573ad317155172346d32442420e5e + sourceBlobDigest: sha256:ac0a3c3a6df002378b3522325c2330ba4309fbbbfa503a04b8267398f20033f5 codeSamplesNamespace: code-samples-java-plexjava - codeSamplesRevisionDigest: sha256:c7bc64aee5441f09dae1f6b15a89c94f6b6065f2e25f5528e849e5ddf2f50d8c + codeSamplesRevisionDigest: sha256:da1aba30a925f7fffb273f3f1f7d25920debd20e62c974ad4a6904a98697c8db workflow: workflowVersion: 1.0.0 speakeasyVersion: latest diff --git a/README.md b/README.md index a226adc8..c25c6095 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ The samples below show how a published SDK artifact is used: Gradle: ```groovy -implementation 'dev.plexapi:plexapi:0.14.2' +implementation 'dev.plexapi:plexapi:0.15.0' ``` Maven: @@ -77,7 +77,7 @@ Maven: dev.plexapi plexapi - 0.14.2 + 0.15.0 ``` @@ -94,6 +94,29 @@ On Windows: ```bash gradlew.bat publishToMavenLocal -Pskip.signing ``` + +### Logging +A logging framework/facade has not yet been adopted but is under consideration. + +For request and response logging (especially json bodies) use: +```java +SpeakeasyHTTPClient.setDebugLogging(true); // experimental API only (may change without warning) +``` +Example output: +``` +Sending request: http://localhost:35123/bearer#global GET +Request headers: {Accept=[application/json], Authorization=[******], Client-Level-Header=[added by client], Idempotency-Key=[some-key], x-speakeasy-user-agent=[speakeasy-sdk/java 0.0.1 internal 0.1.0 org.openapis.openapi]} +Received response: (GET http://localhost:35123/bearer#global) 200 +Response headers: {access-control-allow-credentials=[true], access-control-allow-origin=[*], connection=[keep-alive], content-length=[50], content-type=[application/json], date=[Wed, 09 Apr 2025 01:43:29 GMT], server=[gunicorn/19.9.0]} +Response body: +{ + "authenticated": true, + "token": "global" +} +``` +WARNING: This should only used for temporary debugging purposes. Leaving this option on in a production system could expose credentials/secrets in logs. Authorization headers are redacted by default and there is the ability to specify redacted header names via `SpeakeasyHTTPClient.setRedactedHeaders`. + +Another option is to set the System property `-Djdk.httpclient.HttpClient.log=all`. However, this second option does not log bodies. diff --git a/RELEASES.md b/RELEASES.md index d2d2bacf..657c2738 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -308,4 +308,14 @@ Based on: ### Generated - [java v0.14.2] . ### Releases -- [Maven Central v0.14.2] https://central.sonatype.com/artifact/dev.plexapi/plexapi/0.14.2 - . \ No newline at end of file +- [Maven Central v0.14.2] https://central.sonatype.com/artifact/dev.plexapi/plexapi/0.14.2 - . + +## 2025-04-14 00:30:37 +### Changes +Based on: +- OpenAPI Doc +- Speakeasy CLI 1.531.4 (2.570.4) https://github.com/speakeasy-api/speakeasy +### Generated +- [java v0.15.0] . +### Releases +- [Maven Central v0.15.0] https://central.sonatype.com/artifact/dev.plexapi/plexapi/0.15.0 - . \ No newline at end of file diff --git a/build.gradle b/build.gradle index 19ec528e..b20aea75 100644 --- a/build.gradle +++ b/build.gradle @@ -103,7 +103,7 @@ publishing { // https://github.com/gradle/gradle/issues/18619 groupId = "dev.plexapi" artifactId = "plexapi" - version = "0.14.2" + version = "0.15.0" from components.java diff --git a/docs/models/operations/GetAllLibrariesDirectory.md b/docs/models/operations/GetAllLibrariesDirectory.md index 87b26cc8..d97fbb7f 100644 --- a/docs/models/operations/GetAllLibrariesDirectory.md +++ b/docs/models/operations/GetAllLibrariesDirectory.md @@ -3,26 +3,26 @@ ## Fields -| Field | Type | Required | Description | Example | -| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | -| `allowSync` | *boolean* | :heavy_check_mark: | Indicates whether syncing is allowed. | false | -| `art` | *String* | :heavy_check_mark: | URL for the background artwork of the media container. | /:/resources/show-fanart.jpg | -| `composite` | *String* | :heavy_check_mark: | The relative path to the composite media item. | /library/sections/1/composite/1743824484 | -| `filters` | *boolean* | :heavy_check_mark: | UNKNOWN | true | -| `refreshing` | *boolean* | :heavy_check_mark: | Indicates whether the library is currently being refreshed or updated | true | -| `thumb` | *String* | :heavy_check_mark: | URL for the thumbnail image of the media container. | /:/resources/show.png | -| `key` | *String* | :heavy_check_mark: | The library key representing the unique identifier | 1 | -| `type` | [GetAllLibrariesType](../../models/operations/GetAllLibrariesType.md) | :heavy_check_mark: | N/A | movie | -| `title` | *String* | :heavy_check_mark: | The title of the library | Movies | -| `agent` | *String* | :heavy_check_mark: | The Plex agent used to match and retrieve media metadata. | tv.plex.agents.movie | -| `scanner` | *String* | :heavy_check_mark: | UNKNOWN | Plex Movie | -| `language` | *String* | :heavy_check_mark: | The Plex library language that has been set | en-US | -| `uuid` | *String* | :heavy_check_mark: | The universally unique identifier for the library. | e69655a2-ef48-4aba-bb19-01e7d3cc34d6 | -| `updatedAt` | *long* | :heavy_check_mark: | Unix epoch datetime in seconds | 1556281940 | -| `createdAt` | *Optional\* | :heavy_minus_sign: | N/A | 1556281940 | -| `scannedAt` | *long* | :heavy_check_mark: | Unix epoch datetime in seconds | 1556281940 | -| `content` | *boolean* | :heavy_check_mark: | UNKNOWN | true | -| `directory` | *boolean* | :heavy_check_mark: | UNKNOWN | true | -| `contentChangedAt` | *int* | :heavy_check_mark: | The number of seconds since the content was last changed relative to now. | 9173960 | -| `hidden` | [Optional\](../../models/operations/Hidden.md) | :heavy_minus_sign: | N/A | 1 | -| `location` | List\<[GetAllLibrariesLocation](../../models/operations/GetAllLibrariesLocation.md)> | :heavy_check_mark: | N/A | | \ No newline at end of file +| Field | Type | Required | Description | Example | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `allowSync` | *boolean* | :heavy_check_mark: | Indicates whether syncing is allowed. | false | +| `art` | *String* | :heavy_check_mark: | URL for the background artwork of the media container. | /:/resources/show-fanart.jpg | +| `composite` | *String* | :heavy_check_mark: | The relative path to the composite media item. | /library/sections/1/composite/1743824484 | +| `filters` | *boolean* | :heavy_check_mark: | UNKNOWN | true | +| `refreshing` | *boolean* | :heavy_check_mark: | Indicates whether the library is currently being refreshed or updated | true | +| `thumb` | *String* | :heavy_check_mark: | URL for the thumbnail image of the media container. | /:/resources/show.png | +| `key` | *String* | :heavy_check_mark: | The library key representing the unique identifier | 1 | +| `type` | [GetAllLibrariesType](../../models/operations/GetAllLibrariesType.md) | :heavy_check_mark: | N/A | movie | +| `title` | *String* | :heavy_check_mark: | The title of the library | Movies | +| `agent` | *String* | :heavy_check_mark: | The Plex agent used to match and retrieve media metadata. | tv.plex.agents.movie | +| `scanner` | *String* | :heavy_check_mark: | UNKNOWN | Plex Movie | +| `language` | *String* | :heavy_check_mark: | The Plex library language that has been set | en-US | +| `uuid` | *String* | :heavy_check_mark: | The universally unique identifier for the library. | e69655a2-ef48-4aba-bb19-01e7d3cc34d6 | +| `updatedAt` | *long* | :heavy_check_mark: | Unix epoch datetime in seconds | 1556281940 | +| `createdAt` | *Optional\* | :heavy_minus_sign: | N/A | 1556281940 | +| `scannedAt` | *long* | :heavy_check_mark: | Unix epoch datetime in seconds | 1556281940 | +| `content` | *boolean* | :heavy_check_mark: | UNKNOWN | true | +| `directory` | *boolean* | :heavy_check_mark: | UNKNOWN | true | +| `contentChangedAt` | *long* | :heavy_check_mark: | Timestamp (in seconds) representing the last time the content was modified.
NOTE: Some Plex server have some absurd values for this field, like 8457612157633039800 so it should be int64
| 9173960 | +| `hidden` | [Optional\](../../models/operations/Hidden.md) | :heavy_minus_sign: | N/A | 1 | +| `location` | List\<[GetAllLibrariesLocation](../../models/operations/GetAllLibrariesLocation.md)> | :heavy_check_mark: | N/A | | \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 785cbeec..9565c579 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ groupId=dev.plexapi artifactId=plexapi -version=0.14.2 \ No newline at end of file +version=0.15.0 \ No newline at end of file diff --git a/src/main/java/dev/plexapi/sdk/PlexAPI.java b/src/main/java/dev/plexapi/sdk/PlexAPI.java index 131f62b5..f67e2225 100644 --- a/src/main/java/dev/plexapi/sdk/PlexAPI.java +++ b/src/main/java/dev/plexapi/sdk/PlexAPI.java @@ -52,7 +52,7 @@ public class PlexAPI { /** * The full address of your Plex Server */ - "https://10.10.10.47:32400", + "{protocol}://{ip}:{port}", }; /** diff --git a/src/main/java/dev/plexapi/sdk/SDKConfiguration.java b/src/main/java/dev/plexapi/sdk/SDKConfiguration.java index af8d5b4a..d88ed5b3 100644 --- a/src/main/java/dev/plexapi/sdk/SDKConfiguration.java +++ b/src/main/java/dev/plexapi/sdk/SDKConfiguration.java @@ -42,8 +42,8 @@ class SDKConfiguration { } }; private static final String LANGUAGE = "java"; public static final String OPENAPI_DOC_VERSION = "0.0.3"; - public static final String SDK_VERSION = "0.14.2"; - public static final String GEN_VERSION = "2.566.5"; + public static final String SDK_VERSION = "0.15.0"; + public static final String GEN_VERSION = "2.570.4"; private static final String BASE_PACKAGE = "dev.plexapi.sdk"; public static final String USER_AGENT = String.format("speakeasy-sdk/%s %s %s %s %s", diff --git a/src/main/java/dev/plexapi/sdk/models/operations/GetAllLibrariesDirectory.java b/src/main/java/dev/plexapi/sdk/models/operations/GetAllLibrariesDirectory.java index 9c1e4d6e..478b6fc0 100644 --- a/src/main/java/dev/plexapi/sdk/models/operations/GetAllLibrariesDirectory.java +++ b/src/main/java/dev/plexapi/sdk/models/operations/GetAllLibrariesDirectory.java @@ -12,7 +12,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import dev.plexapi.sdk.utils.LazySingletonValue; import dev.plexapi.sdk.utils.Utils; import java.lang.Boolean; -import java.lang.Integer; import java.lang.Long; import java.lang.Override; import java.lang.String; @@ -127,10 +126,11 @@ public class GetAllLibrariesDirectory { private boolean directory; /** - * The number of seconds since the content was last changed relative to now. + * Timestamp (in seconds) representing the last time the content was modified. + * NOTE: Some Plex server have some absurd values for this field, like 8457612157633039800 so it should be int64 */ @JsonProperty("contentChangedAt") - private int contentChangedAt; + private long contentChangedAt; @JsonInclude(Include.NON_ABSENT) @JsonProperty("hidden") @@ -159,7 +159,7 @@ public class GetAllLibrariesDirectory { @JsonProperty("scannedAt") long scannedAt, @JsonProperty("content") boolean content, @JsonProperty("directory") boolean directory, - @JsonProperty("contentChangedAt") int contentChangedAt, + @JsonProperty("contentChangedAt") long contentChangedAt, @JsonProperty("hidden") Optional hidden, @JsonProperty("Location") List location) { Utils.checkNotNull(allowSync, "allowSync"); @@ -224,7 +224,7 @@ public class GetAllLibrariesDirectory { long scannedAt, boolean content, boolean directory, - int contentChangedAt, + long contentChangedAt, List location) { this(allowSync, art, composite, filters, refreshing, thumb, key, type, title, agent, scanner, language, uuid, updatedAt, Optional.empty(), scannedAt, content, directory, contentChangedAt, Optional.empty(), location); } @@ -368,10 +368,11 @@ public class GetAllLibrariesDirectory { } /** - * The number of seconds since the content was last changed relative to now. + * Timestamp (in seconds) representing the last time the content was modified. + * NOTE: Some Plex server have some absurd values for this field, like 8457612157633039800 so it should be int64 */ @JsonIgnore - public int contentChangedAt() { + public long contentChangedAt() { return contentChangedAt; } @@ -553,9 +554,10 @@ public class GetAllLibrariesDirectory { } /** - * The number of seconds since the content was last changed relative to now. + * Timestamp (in seconds) representing the last time the content was modified. + * NOTE: Some Plex server have some absurd values for this field, like 8457612157633039800 so it should be int64 */ - public GetAllLibrariesDirectory withContentChangedAt(int contentChangedAt) { + public GetAllLibrariesDirectory withContentChangedAt(long contentChangedAt) { Utils.checkNotNull(contentChangedAt, "contentChangedAt"); this.contentChangedAt = contentChangedAt; return this; @@ -703,7 +705,7 @@ public class GetAllLibrariesDirectory { private Boolean directory; - private Integer contentChangedAt; + private Long contentChangedAt; private Optional hidden; @@ -876,9 +878,10 @@ public class GetAllLibrariesDirectory { } /** - * The number of seconds since the content was last changed relative to now. + * Timestamp (in seconds) representing the last time the content was modified. + * NOTE: Some Plex server have some absurd values for this field, like 8457612157633039800 so it should be int64 */ - public Builder contentChangedAt(int contentChangedAt) { + public Builder contentChangedAt(long contentChangedAt) { Utils.checkNotNull(contentChangedAt, "contentChangedAt"); this.contentChangedAt = contentChangedAt; return this; diff --git a/src/main/java/dev/plexapi/sdk/utils/SpeakeasyHTTPClient.java b/src/main/java/dev/plexapi/sdk/utils/SpeakeasyHTTPClient.java index afc46023..7b4631ff 100644 --- a/src/main/java/dev/plexapi/sdk/utils/SpeakeasyHTTPClient.java +++ b/src/main/java/dev/plexapi/sdk/utils/SpeakeasyHTTPClient.java @@ -4,18 +4,139 @@ package dev.plexapi.sdk.utils; import java.io.IOException; +import java.io.InputStream; import java.net.URISyntaxException; import java.net.http.HttpClient; +import java.net.http.HttpHeaders; import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; -import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Locale; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; public class SpeakeasyHTTPClient implements HTTPClient { + private static boolean debugEnabled = false; + + // uppercase + private static Set redactedHeaders = Set.of("AUTHORIZATION", "X-API-KEY"); + + private static Consumer logger = System.out::println; + + /** + * Experimental, may be changed anytime. Sets debug logging on or off for + * requests and responses including bodies for JSON content. WARNING: this + * setting may expose sensitive information in logs (like Authorization + * headers), and should only be enabled temporarily for local debugging + * purposes. By default, Authorization headers are redacted in the logs + * ( printed with a value of {@code [*******]}). Header suppression is controlled + * with the {@link #setRedactedHeaders(Collection)} method. + * + * @param enabled true to enable debug logging, false to disable it + */ + public static void setDebugLogging(boolean enabled) { + debugEnabled = enabled; + } + + /** + * Experimental, may be changed anytime. When debug logging is enabled this + * method controls the suppression of header values in the logs. By default, + * Authorization headers are redacted in the logs (printed with a value + * of {@code [*******]}). Header suppression is controlled with the + * {@link #setRedactedHeaders(Collection)} method. + * + * @param headerNames the names (case-insensitive) of the headers whose values + * will be redacted in the logs + */ + public static void setRedactedHeaders(Collection headerNames) { + redactedHeaders = headerNames.stream() // + .map(x -> x.toUpperCase(Locale.ENGLISH)) // + .collect(Collectors.toSet()); + } + + public static void setLogger(Consumer logger) { + SpeakeasyHTTPClient.logger = logger; + } + @Override public HttpResponse send(HttpRequest request) throws IOException, InterruptedException, URISyntaxException { HttpClient client = HttpClient.newHttpClient(); - return client.send(request, HttpResponse.BodyHandlers.ofInputStream()); + if (debugEnabled) { + request = logRequest(request); + } + var response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()); + if (debugEnabled) { + response = logResponse(response); + } + return response; + } + + private HttpRequest logRequest(HttpRequest request) { + log("Sending request: " + request); + log("Request headers: " + redactHeaders(request.headers())); + // only log the body if it is present and the content type is JSON + if (request.bodyPublisher().isPresent() && request.headers() // + .firstValue("Content-Type") // + .filter(x -> x.equals("application/json") || x.equals("text/plain")).isPresent()) { + // we read the body and ensure that the BodyPublisher is rebuilt to pass to the + // http client + byte[] body = Helpers.bodyBytes(request); + request = Helpers // + .copy(request) // + .method(request.method(), BodyPublishers.ofByteArray(body)) // + .build(); + // note that in the case of text/plain a different encoding from UTF-8 + // may be in use but we just log the bytes as UTF-8. Unexpected encodings + // do not throw (substitution happens). + log("Request body:\n" + new String(body, StandardCharsets.UTF_8)); + } + return request; + } + + private static HttpResponse logResponse(HttpResponse response) throws IOException { + // make the response re-readable by loading the response body into a byte array + // and allowing the InputStream to be read many times + response = Utils.cache(response); + log("Received response: " + response); + log("Response headers: " + redactHeaders(response.headers())); + // only log the response body if it is present and the content type is JSON or plain text + if (response.headers() // + .firstValue("Content-Type") // + .filter(x -> x.equals("application/json") || x.equals("text/plain")) // + .isPresent()) { + // the response is re-readable so we can read and close it without + // affecting later processing of the response. + + // note that in the case of text/plain a different encoding from UTF-8 + // may be in use but we just log the bytes as UTF-8. Unexpected encodings + // do not throw (substitution happens). + log("Response body:\n" + Utils.toUtf8AndClose(response.body())); + } + return response; + } + + private static String redactHeaders(HttpHeaders headers) { + return "{" + headers.map() // + .entrySet() // + .stream() // + .map(entry -> { + final String value; + if (redactedHeaders.contains(entry.getKey().toUpperCase(Locale.ENGLISH))) { + value = "[******]"; + } else { + value = String.valueOf(entry.getValue()); + } + return entry.getKey() + "=" + value; + }) // + .collect(Collectors.joining(", ")) + "}"; + } + + private static void log(String message) { + logger.accept(message); } } diff --git a/src/main/java/dev/plexapi/sdk/utils/Utils.java b/src/main/java/dev/plexapi/sdk/utils/Utils.java index e6d36db1..2884f96a 100644 --- a/src/main/java/dev/plexapi/sdk/utils/Utils.java +++ b/src/main/java/dev/plexapi/sdk/utils/Utils.java @@ -1368,20 +1368,32 @@ public final class Utils { } return list; } - - public static T valueOrNull(T value) { - return value; + + public static T valueOrElse(T value, T valueIfNotPresent) { + return value != null ? value : valueIfNotPresent; + } + + public static T valueOrElse(Optional value, T valueIfNotPresent) { + return value.orElse(valueIfNotPresent); } - public static T valueOrNull(Optional value) { - return value.orElse(null); - } - - public static T valueOrNull(JsonNullable value) { + public static T valueOrElse(JsonNullable value, T valueIfNotPresent) { if (value.isPresent()) { return value.get(); } else { - return null; + return valueIfNotPresent; } } + + public static T valueOrNull(T value) { + return valueOrElse(value, null); + } + + public static T valueOrNull(Optional value) { + return valueOrElse(value, null); + } + + public static T valueOrNull(JsonNullable value) { + return valueOrElse(value, null); + } }