diff --git a/.speakeasy/gen.lock b/.speakeasy/gen.lock index e62b6f3..fe3645c 100755 --- a/.speakeasy/gen.lock +++ b/.speakeasy/gen.lock @@ -1,29 +1,29 @@ lockVersion: 2.0.0 id: dfa99515-01c0-42eb-9be5-ee212fd03eb3 management: - docChecksum: 099fc5bbb987ae7e391f7f07b4212860 + docChecksum: 278d186496d83d5830ba4870f8cad39d docVersion: 0.0.3 speakeasyVersion: internal - generationVersion: 2.249.1 - releaseVersion: 0.3.0 - configChecksum: ae48b3df080bde21a30ed099e1ffc0d8 + generationVersion: 2.269.0 + releaseVersion: 0.4.0 + configChecksum: 319de8bf53c668d84b706d9496459b7c repoURL: https://github.com/LukeHagar/plexgo.git repoSubDirectory: . installationURL: https://github.com/LukeHagar/plexgo published: true features: go: - constsAndDefaults: 0.1.2 - core: 3.3.2 + constsAndDefaults: 0.1.3 + core: 3.4.1 flattening: 2.81.1 globalSecurity: 2.82.6 globalServerURLs: 2.82.1 methodServerURLs: 2.82.1 nameOverrides: 2.81.1 - unions: 2.85.2 generatedFiles: - server.go - media.go + - video.go - activities.go - butler.go - hubs.go @@ -33,12 +33,11 @@ generatedFiles: - plex.go - playlists.go - security.go + - statistics.go - sessions.go - updater.go - - video.go - plexapi.go - go.mod - - go.sum - models/sdkerrors/sdkerror.go - types/bigint.go - types/date.go @@ -66,6 +65,8 @@ generatedFiles: - /models/operations/markplayed.go - /models/operations/markunplayed.go - /models/operations/updateplayprogress.go + - /models/operations/gettimeline.go + - /models/operations/startuniversaltranscode.go - /models/operations/getserveractivities.go - /models/operations/cancelserveractivities.go - /models/operations/getbutlertasks.go @@ -105,6 +106,7 @@ generatedFiles: - /models/operations/uploadplaylist.go - /models/operations/gettransienttoken.go - /models/operations/getsourceconnectioninformation.go + - /models/operations/getstatistics.go - /models/operations/getsessions.go - /models/operations/getsessionhistory.go - /models/operations/gettranscodesessions.go @@ -112,8 +114,6 @@ generatedFiles: - /models/operations/getupdatestatus.go - /models/operations/checkforupdates.go - /models/operations/applyupdates.go - - /models/operations/startuniversaltranscode.go - - /models/operations/gettimeline.go - /models/sdkerrors/getservercapabilities.go - /models/sdkerrors/getserverpreferences.go - /models/sdkerrors/getavailableclients.go @@ -125,6 +125,8 @@ generatedFiles: - /models/sdkerrors/markplayed.go - /models/sdkerrors/markunplayed.go - /models/sdkerrors/updateplayprogress.go + - /models/sdkerrors/gettimeline.go + - /models/sdkerrors/startuniversaltranscode.go - /models/sdkerrors/getserveractivities.go - /models/sdkerrors/cancelserveractivities.go - /models/sdkerrors/getbutlertasks.go @@ -162,6 +164,7 @@ generatedFiles: - /models/sdkerrors/uploadplaylist.go - /models/sdkerrors/gettransienttoken.go - /models/sdkerrors/getsourceconnectioninformation.go + - /models/sdkerrors/getstatistics.go - /models/sdkerrors/getsessions.go - /models/sdkerrors/getsessionhistory.go - /models/sdkerrors/gettranscodesessions.go @@ -169,15 +172,11 @@ generatedFiles: - /models/sdkerrors/getupdatestatus.go - /models/sdkerrors/checkforupdates.go - /models/sdkerrors/applyupdates.go - - /models/sdkerrors/startuniversaltranscode.go - - /models/sdkerrors/gettimeline.go - /models/components/security.go - docs/models/operations/directory.md - docs/models/operations/mediacontainer.md - docs/models/operations/getservercapabilitiesresponsebody.md - docs/models/operations/getservercapabilitiesresponse.md - - docs/models/operations/two.md - - docs/models/operations/one.md - docs/models/operations/setting.md - docs/models/operations/getserverpreferencesmediacontainer.md - docs/models/operations/getserverpreferencesresponsebody.md @@ -210,6 +209,11 @@ generatedFiles: - docs/models/operations/markunplayedresponse.md - docs/models/operations/updateplayprogressrequest.md - docs/models/operations/updateplayprogressresponse.md + - docs/models/operations/state.md + - docs/models/operations/gettimelinerequest.md + - docs/models/operations/gettimelineresponse.md + - docs/models/operations/startuniversaltranscoderequest.md + - docs/models/operations/startuniversaltranscoderesponse.md - docs/models/operations/context.md - docs/models/operations/activity.md - docs/models/operations/getserveractivitiesmediacontainer.md @@ -413,6 +417,13 @@ generatedFiles: - docs/models/operations/gettransienttokenresponse.md - docs/models/operations/getsourceconnectioninformationrequest.md - docs/models/operations/getsourceconnectioninformationresponse.md + - docs/models/operations/getstatisticsrequest.md + - docs/models/operations/getstatisticsdevice.md + - docs/models/operations/account.md + - docs/models/operations/statisticsmedia.md + - docs/models/operations/getstatisticsmediacontainer.md + - docs/models/operations/getstatisticsresponsebody.md + - docs/models/operations/getstatisticsresponse.md - docs/models/operations/getsessionsstream.md - docs/models/operations/getsessionspart.md - docs/models/operations/getsessionsmedia.md @@ -444,11 +455,6 @@ generatedFiles: - docs/models/operations/skip.md - docs/models/operations/applyupdatesrequest.md - docs/models/operations/applyupdatesresponse.md - - docs/models/operations/startuniversaltranscoderequest.md - - docs/models/operations/startuniversaltranscoderesponse.md - - docs/models/operations/state.md - - docs/models/operations/gettimelinerequest.md - - docs/models/operations/gettimelineresponse.md - docs/models/sdkerrors/errors.md - docs/models/sdkerrors/getservercapabilitiesresponsebody.md - docs/models/sdkerrors/getserverpreferenceserrors.md @@ -471,6 +477,10 @@ generatedFiles: - docs/models/sdkerrors/markunplayedresponsebody.md - docs/models/sdkerrors/updateplayprogresserrors.md - docs/models/sdkerrors/updateplayprogressresponsebody.md + - docs/models/sdkerrors/gettimelineerrors.md + - docs/models/sdkerrors/gettimelineresponsebody.md + - docs/models/sdkerrors/startuniversaltranscodeerrors.md + - docs/models/sdkerrors/startuniversaltranscoderesponsebody.md - docs/models/sdkerrors/getserveractivitieserrors.md - docs/models/sdkerrors/getserveractivitiesresponsebody.md - docs/models/sdkerrors/cancelserveractivitieserrors.md @@ -545,6 +555,8 @@ generatedFiles: - docs/models/sdkerrors/gettransienttokenresponsebody.md - docs/models/sdkerrors/getsourceconnectioninformationerrors.md - docs/models/sdkerrors/getsourceconnectioninformationresponsebody.md + - docs/models/sdkerrors/getstatisticserrors.md + - docs/models/sdkerrors/getstatisticsresponsebody.md - docs/models/sdkerrors/getsessionserrors.md - docs/models/sdkerrors/getsessionsresponsebody.md - docs/models/sdkerrors/getsessionhistoryerrors.md @@ -559,14 +571,11 @@ generatedFiles: - docs/models/sdkerrors/checkforupdatesresponsebody.md - docs/models/sdkerrors/applyupdateserrors.md - docs/models/sdkerrors/applyupdatesresponsebody.md - - docs/models/sdkerrors/startuniversaltranscodeerrors.md - - docs/models/sdkerrors/startuniversaltranscoderesponsebody.md - - docs/models/sdkerrors/gettimelineerrors.md - - docs/models/sdkerrors/gettimelineresponsebody.md - docs/models/components/security.md - docs/sdks/plexapi/README.md - docs/sdks/server/README.md - docs/sdks/media/README.md + - docs/sdks/video/README.md - docs/sdks/activities/README.md - docs/sdks/butler/README.md - docs/sdks/hubs/README.md @@ -577,9 +586,10 @@ generatedFiles: - docs/sdks/plex/README.md - docs/sdks/playlists/README.md - docs/sdks/security/README.md + - docs/sdks/statistics/README.md - docs/sdks/sessions/README.md - docs/sdks/updater/README.md - - docs/sdks/video/README.md - USAGE.md - models/operations/options.go - .gitattributes + - internal/hooks/hooks.go diff --git a/README.md b/README.md index eae7ccb..0de846b 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,11 @@ func main() { * [MarkUnplayed](docs/sdks/media/README.md#markunplayed) - Mark Media Unplayed * [UpdatePlayProgress](docs/sdks/media/README.md#updateplayprogress) - Update Media Play Progress +### [Video](docs/sdks/video/README.md) + +* [GetTimeline](docs/sdks/video/README.md#gettimeline) - Get the timeline for a media item +* [StartUniversalTranscode](docs/sdks/video/README.md#startuniversaltranscode) - Start Universal Transcode + ### [Activities](docs/sdks/activities/README.md) * [GetServerActivities](docs/sdks/activities/README.md#getserveractivities) - Get Server Activities @@ -135,6 +140,10 @@ func main() { * [GetTransientToken](docs/sdks/security/README.md#gettransienttoken) - Get a Transient Token. * [GetSourceConnectionInformation](docs/sdks/security/README.md#getsourceconnectioninformation) - Get Source Connection Information +### [Statistics](docs/sdks/statistics/README.md) + +* [GetStatistics](docs/sdks/statistics/README.md#getstatistics) - Get Media Statistics + ### [Sessions](docs/sdks/sessions/README.md) * [GetSessions](docs/sdks/sessions/README.md#getsessions) - Get Active Sessions @@ -147,11 +156,6 @@ func main() { * [GetUpdateStatus](docs/sdks/updater/README.md#getupdatestatus) - Querying status of updates * [CheckForUpdates](docs/sdks/updater/README.md#checkforupdates) - Checking for updates * [ApplyUpdates](docs/sdks/updater/README.md#applyupdates) - Apply Updates - -### [Video](docs/sdks/video/README.md) - -* [StartUniversalTranscode](docs/sdks/video/README.md#startuniversaltranscode) - Start Universal Transcode -* [GetTimeline](docs/sdks/video/README.md#gettimeline) - Get the timeline for a media item @@ -303,9 +307,9 @@ func main() { plexgo.WithSecurity(""), ) - var xPlexClientIdentifier string = "string" + var xPlexClientIdentifier string = "" - var strong *bool = false + var strong *bool = plexgo.Bool(false) ctx := context.Background() res, err := s.Plex.GetPin(ctx, operations.WithServerURL("https://plex.tv/api/v2"), xPlexClientIdentifier, strong) diff --git a/RELEASES.md b/RELEASES.md index ac28bb8..2eefdab 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -118,4 +118,14 @@ Based on: ### Generated - [go v0.3.0] . ### Releases -- [Go v0.3.0] https://github.com/LukeHagar/plexgo/releases/tag/v0.3.0 - . \ No newline at end of file +- [Go v0.3.0] https://github.com/LukeHagar/plexgo/releases/tag/v0.3.0 - . + +## 2024-02-23 14:46:30 +### Changes +Based on: +- OpenAPI Doc 0.0.3 +- Speakeasy CLI 1.193.0 (2.269.0) https://github.com/speakeasy-api/speakeasy +### Generated +- [go v0.4.0] . +### Releases +- [Go v0.4.0] https://github.com/LukeHagar/plexgo/releases/tag/v0.4.0 - . \ No newline at end of file diff --git a/activities.go b/activities.go index f41dc05..3a426cd 100644 --- a/activities.go +++ b/activities.go @@ -6,12 +6,13 @@ import ( "bytes" "context" "fmt" + "github.com/LukeHagar/plexgo/internal/hooks" "github.com/LukeHagar/plexgo/internal/utils" "github.com/LukeHagar/plexgo/models/operations" "github.com/LukeHagar/plexgo/models/sdkerrors" "io" "net/http" - "strings" + "net/url" ) // Activities are awesome. They provide a way to monitor and control asynchronous operations on the server. In order to receive real-time updates for activities, a client would normally subscribe via either EventSource or Websocket endpoints. @@ -34,26 +35,49 @@ func newActivities(sdkConfig sdkConfiguration) *Activities { // GetServerActivities - Get Server Activities // Get Server Activities func (s *Activities) GetServerActivities(ctx context.Context) (*operations.GetServerActivitiesResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/activities" + hookCtx := hooks.HookContext{OperationID: "getServerActivities"} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/activities") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetServerActivitiesResponse{ @@ -68,6 +92,7 @@ func (s *Activities) GetServerActivities(ctx context.Context) (*operations.GetSe } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -108,33 +133,53 @@ func (s *Activities) GetServerActivities(ctx context.Context) (*operations.GetSe // CancelServerActivities - Cancel Server Activities // Cancel Server Activities func (s *Activities) CancelServerActivities(ctx context.Context, activityUUID string) (*operations.CancelServerActivitiesResponse, error) { + hookCtx := hooks.HookContext{OperationID: "cancelServerActivities"} + request := operations.CancelServerActivitiesRequest{ ActivityUUID: activityUUID, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url, err := utils.GenerateURL(ctx, baseURL, "/activities/{activityUUID}", request, nil) + opURL, err := utils.GenerateURL(ctx, baseURL, "/activities/{activityUUID}", request, nil) if err != nil { return nil, fmt.Errorf("error generating URL: %w", err) } - req, err := http.NewRequestWithContext(ctx, "DELETE", url, nil) + req, err := http.NewRequestWithContext(ctx, "DELETE", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.CancelServerActivitiesResponse{ @@ -149,6 +194,7 @@ func (s *Activities) CancelServerActivities(ctx context.Context, activityUUID st } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: diff --git a/butler.go b/butler.go index 06ba299..b6dbecd 100644 --- a/butler.go +++ b/butler.go @@ -6,12 +6,13 @@ import ( "bytes" "context" "fmt" + "github.com/LukeHagar/plexgo/internal/hooks" "github.com/LukeHagar/plexgo/internal/utils" "github.com/LukeHagar/plexgo/models/operations" "github.com/LukeHagar/plexgo/models/sdkerrors" "io" "net/http" - "strings" + "net/url" ) // Butler is the task manager of the Plex Media Server Ecosystem. @@ -28,26 +29,49 @@ func newButler(sdkConfig sdkConfiguration) *Butler { // GetButlerTasks - Get Butler tasks // Returns a list of butler tasks func (s *Butler) GetButlerTasks(ctx context.Context) (*operations.GetButlerTasksResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/butler" + hookCtx := hooks.HookContext{OperationID: "getButlerTasks"} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/butler") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetButlerTasksResponse{ @@ -62,6 +86,7 @@ func (s *Butler) GetButlerTasks(ctx context.Context) (*operations.GetButlerTasks } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -106,26 +131,49 @@ func (s *Butler) GetButlerTasks(ctx context.Context) (*operations.GetButlerTasks // 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. func (s *Butler) StartAllTasks(ctx context.Context) (*operations.StartAllTasksResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/butler" + hookCtx := hooks.HookContext{OperationID: "startAllTasks"} - req, err := http.NewRequestWithContext(ctx, "POST", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/butler") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.StartAllTasksResponse{ @@ -140,6 +188,7 @@ func (s *Butler) StartAllTasks(ctx context.Context) (*operations.StartAllTasksRe } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: @@ -169,26 +218,49 @@ func (s *Butler) StartAllTasks(ctx context.Context) (*operations.StartAllTasksRe // StopAllTasks - Stop all Butler tasks // This endpoint will stop all currently running tasks and remove any scheduled tasks from the queue. func (s *Butler) StopAllTasks(ctx context.Context) (*operations.StopAllTasksResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/butler" + hookCtx := hooks.HookContext{OperationID: "stopAllTasks"} - req, err := http.NewRequestWithContext(ctx, "DELETE", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/butler") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "DELETE", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.StopAllTasksResponse{ @@ -203,6 +275,7 @@ func (s *Butler) StopAllTasks(ctx context.Context) (*operations.StopAllTasksResp } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: @@ -236,33 +309,53 @@ func (s *Butler) StopAllTasks(ctx context.Context) (*operations.StopAllTasksResp // 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. func (s *Butler) StartTask(ctx context.Context, taskName operations.TaskName) (*operations.StartTaskResponse, error) { + hookCtx := hooks.HookContext{OperationID: "startTask"} + request := operations.StartTaskRequest{ TaskName: taskName, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url, err := utils.GenerateURL(ctx, baseURL, "/butler/{taskName}", request, nil) + opURL, err := utils.GenerateURL(ctx, baseURL, "/butler/{taskName}", request, nil) if err != nil { return nil, fmt.Errorf("error generating URL: %w", err) } - req, err := http.NewRequestWithContext(ctx, "POST", url, nil) + req, err := http.NewRequestWithContext(ctx, "POST", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.StartTaskResponse{ @@ -277,6 +370,7 @@ func (s *Butler) StartTask(ctx context.Context, taskName operations.TaskName) (* } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: fallthrough @@ -308,33 +402,53 @@ func (s *Butler) StartTask(ctx context.Context, taskName operations.TaskName) (* // StopTask - Stop a single Butler task // 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. func (s *Butler) StopTask(ctx context.Context, taskName operations.PathParamTaskName) (*operations.StopTaskResponse, error) { + hookCtx := hooks.HookContext{OperationID: "stopTask"} + request := operations.StopTaskRequest{ TaskName: taskName, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url, err := utils.GenerateURL(ctx, baseURL, "/butler/{taskName}", request, nil) + opURL, err := utils.GenerateURL(ctx, baseURL, "/butler/{taskName}", request, nil) if err != nil { return nil, fmt.Errorf("error generating URL: %w", err) } - req, err := http.NewRequestWithContext(ctx, "DELETE", url, nil) + req, err := http.NewRequestWithContext(ctx, "DELETE", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "404", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.StopTaskResponse{ @@ -349,6 +463,7 @@ func (s *Butler) StopTask(ctx context.Context, taskName operations.PathParamTask } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: diff --git a/docs/models/operations/account.md b/docs/models/operations/account.md new file mode 100644 index 0000000..5adf9db --- /dev/null +++ b/docs/models/operations/account.md @@ -0,0 +1,15 @@ +# Account + + +## Fields + +| Field | Type | Required | Description | Example | +| ---------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | +| `ID` | **int* | :heavy_minus_sign: | N/A | 238960586 | +| `Key` | **string* | :heavy_minus_sign: | N/A | /accounts/238960586 | +| `Name` | **string* | :heavy_minus_sign: | N/A | Diane | +| `DefaultAudioLanguage` | **string* | :heavy_minus_sign: | N/A | en | +| `AutoSelectAudio` | **bool* | :heavy_minus_sign: | N/A | true | +| `DefaultSubtitleLanguage` | **string* | :heavy_minus_sign: | N/A | en | +| `SubtitleMode` | **int* | :heavy_minus_sign: | N/A | 1 | +| `Thumb` | **string* | :heavy_minus_sign: | N/A | https://plex.tv/users/50d83634246da1de/avatar?c=1707110967 | \ No newline at end of file diff --git a/docs/models/operations/getserverpreferencesmediacontainer.md b/docs/models/operations/getserverpreferencesmediacontainer.md index 7ad9d07..b82c005 100644 --- a/docs/models/operations/getserverpreferencesmediacontainer.md +++ b/docs/models/operations/getserverpreferencesmediacontainer.md @@ -3,7 +3,7 @@ ## Fields -| Field | Type | Required | Description | Example | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `Size` | **int* | :heavy_minus_sign: | N/A | 161 | -| `Setting` | [][operations.Setting](../../models/operations/setting.md) | :heavy_minus_sign: | N/A | [{"advanced":false,"default":"","group":"general","hidden":false,"id":"FriendlyName","label":"Friendly name","summary":"This name will be used to identify this media server to other computers on your network. If you leave it blank, your computer's name will be used instead.","type":"text","value":"Hera"},{"advanced":false,"default":3600,"enumValues":"900:every 15 minutes\|1800:every 30 minutes\|3600:hourly\|7200:every 2 hours\|21600:every 6 hours\|43200:every 12 hours\|86400:daily","group":"library","hidden":false,"id":"ScheduledLibraryUpdateInterval","label":"Library scan interval","summary":"","type":"int","value":3600}] | \ No newline at end of file +| Field | Type | Required | Description | Example | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Size` | **int* | :heavy_minus_sign: | N/A | 161 | +| `Setting` | [][operations.Setting](../../models/operations/setting.md) | :heavy_minus_sign: | N/A | [{"advanced":false,"default":false,"enumValues":"1:admin only\|2:everyone","group":"","hidden":true,"id":"EnableDatabaseTrace","label":"","summary":"","type":"bool","value":false}] | \ No newline at end of file diff --git a/docs/models/operations/getstatisticsdevice.md b/docs/models/operations/getstatisticsdevice.md new file mode 100644 index 0000000..839f5ae --- /dev/null +++ b/docs/models/operations/getstatisticsdevice.md @@ -0,0 +1,12 @@ +# GetStatisticsDevice + + +## Fields + +| Field | Type | Required | Description | Example | +| -------------------------------- | -------------------------------- | -------------------------------- | -------------------------------- | -------------------------------- | +| `ID` | **int* | :heavy_minus_sign: | N/A | 208 | +| `Name` | **string* | :heavy_minus_sign: | N/A | Roku Express | +| `Platform` | **string* | :heavy_minus_sign: | N/A | Roku | +| `ClientIdentifier` | **string* | :heavy_minus_sign: | N/A | 793095d235660625108ef785cc7646e9 | +| `CreatedAt` | **int* | :heavy_minus_sign: | N/A | 1706470556 | \ No newline at end of file diff --git a/docs/models/operations/two.md b/docs/models/operations/getstatisticsmediacontainer.md similarity index 50% rename from docs/models/operations/two.md rename to docs/models/operations/getstatisticsmediacontainer.md index 2c6f26c..9be3128 100644 --- a/docs/models/operations/two.md +++ b/docs/models/operations/getstatisticsmediacontainer.md @@ -1,17 +1,11 @@ -# Two +# GetStatisticsMediaContainer ## Fields -| Field | Type | Required | Description | Example | -| ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | -| `ID` | **string* | :heavy_minus_sign: | N/A | ScheduledLibraryUpdateInterval | -| `Label` | **string* | :heavy_minus_sign: | N/A | Library scan interval | -| `Summary` | **string* | :heavy_minus_sign: | N/A | | -| `Type` | **string* | :heavy_minus_sign: | N/A | int | -| `Default` | **int* | :heavy_minus_sign: | N/A | 3600 | -| `Value` | **int* | :heavy_minus_sign: | N/A | 3600 | -| `Hidden` | **bool* | :heavy_minus_sign: | N/A | false | -| `Advanced` | **bool* | :heavy_minus_sign: | N/A | false | -| `Group` | **string* | :heavy_minus_sign: | N/A | library | -| `EnumValues` | **string* | :heavy_minus_sign: | N/A | 900:every 15 minutes\|1800:every 30 minutes\|3600:hourly\|7200:every 2 hours\|21600:every 6 hours\|43200:every 12 hours\|86400:daily | \ No newline at end of file +| Field | Type | Required | Description | Example | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Size` | **int* | :heavy_minus_sign: | N/A | 5497 | +| `Device` | [][operations.GetStatisticsDevice](../../models/operations/getstatisticsdevice.md) | :heavy_minus_sign: | N/A | [{"clientIdentifier":"793095d235660625108ef785cc7646e9","createdAt":1706470556,"id":208,"name":"Roku Express","platform":"Roku"}] | +| `Account` | [][operations.Account](../../models/operations/account.md) | :heavy_minus_sign: | N/A | [{"autoSelectAudio":true,"defaultAudioLanguage":"en","defaultSubtitleLanguage":"en","id":238960586,"key":"/accounts/238960586","name":"Diane","subtitleMode":1,"thumb":"https://plex.tv/users/50d83634246da1de/avatar?c=1707110967"}] | +| `StatisticsMedia` | [][operations.StatisticsMedia](../../models/operations/statisticsmedia.md) | :heavy_minus_sign: | N/A | [{"accountID":1,"at":1707141600,"count":1,"deviceID":13,"duration":1555,"metadataType":4,"timespan":4}] | \ No newline at end of file diff --git a/docs/models/operations/getstatisticsrequest.md b/docs/models/operations/getstatisticsrequest.md new file mode 100644 index 0000000..29eaf23 --- /dev/null +++ b/docs/models/operations/getstatisticsrequest.md @@ -0,0 +1,8 @@ +# GetStatisticsRequest + + +## Fields + +| Field | Type | Required | Description | +| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| `Timespan` | **int64* | :heavy_minus_sign: | The timespan to retrieve statistics for
the exact meaning of this parameter is not known
| \ No newline at end of file diff --git a/docs/models/operations/getstatisticsresponse.md b/docs/models/operations/getstatisticsresponse.md new file mode 100644 index 0000000..2d108e2 --- /dev/null +++ b/docs/models/operations/getstatisticsresponse.md @@ -0,0 +1,11 @@ +# GetStatisticsResponse + + +## Fields + +| Field | Type | Required | Description | +| --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| `ContentType` | *string* | :heavy_check_mark: | HTTP response content type for this operation | +| `StatusCode` | *int* | :heavy_check_mark: | HTTP response status code for this operation | +| `RawResponse` | [*http.Response](https://pkg.go.dev/net/http#Response) | :heavy_check_mark: | Raw HTTP response; suitable for custom response parsing | +| `Object` | [*operations.GetStatisticsResponseBody](../../models/operations/getstatisticsresponsebody.md) | :heavy_minus_sign: | Media Statistics | \ No newline at end of file diff --git a/docs/models/operations/getstatisticsresponsebody.md b/docs/models/operations/getstatisticsresponsebody.md new file mode 100644 index 0000000..be81584 --- /dev/null +++ b/docs/models/operations/getstatisticsresponsebody.md @@ -0,0 +1,10 @@ +# GetStatisticsResponseBody + +Media Statistics + + +## Fields + +| Field | Type | Required | Description | +| ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| `MediaContainer` | [*operations.GetStatisticsMediaContainer](../../models/operations/getstatisticsmediacontainer.md) | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/models/operations/one.md b/docs/models/operations/one.md deleted file mode 100644 index 12213b0..0000000 --- a/docs/models/operations/one.md +++ /dev/null @@ -1,16 +0,0 @@ -# One - - -## Fields - -| Field | Type | Required | Description | Example | -| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ID` | **string* | :heavy_minus_sign: | N/A | FriendlyName | -| `Label` | **string* | :heavy_minus_sign: | N/A | Friendly name | -| `Summary` | **string* | :heavy_minus_sign: | N/A | This name will be used to identify this media server to other computers on your network. If you leave it blank, your computer's name will be used instead. | -| `Type` | **string* | :heavy_minus_sign: | N/A | text | -| `Default` | **string* | :heavy_minus_sign: | N/A | | -| `Value` | **string* | :heavy_minus_sign: | N/A | Hera | -| `Hidden` | **bool* | :heavy_minus_sign: | N/A | false | -| `Advanced` | **bool* | :heavy_minus_sign: | N/A | false | -| `Group` | **string* | :heavy_minus_sign: | N/A | general | \ No newline at end of file diff --git a/docs/models/operations/setting.md b/docs/models/operations/setting.md index b980567..be84858 100644 --- a/docs/models/operations/setting.md +++ b/docs/models/operations/setting.md @@ -1,17 +1,17 @@ # Setting -## Supported Types - -### One - -```go -setting := operations.CreateSettingOne(operations.One{/* values here */}) -``` - -### Two - -```go -setting := operations.CreateSettingTwo(operations.Two{/* values here */}) -``` +## Fields +| Field | Type | Required | Description | Example | +| ----------------------- | ----------------------- | ----------------------- | ----------------------- | ----------------------- | +| `ID` | **string* | :heavy_minus_sign: | N/A | EnableDatabaseTrace | +| `Label` | **string* | :heavy_minus_sign: | N/A | | +| `Summary` | **string* | :heavy_minus_sign: | N/A | | +| `Type` | **string* | :heavy_minus_sign: | N/A | bool | +| `Default` | **bool* | :heavy_minus_sign: | N/A | false | +| `Value` | **bool* | :heavy_minus_sign: | N/A | false | +| `Hidden` | **bool* | :heavy_minus_sign: | N/A | true | +| `Advanced` | **bool* | :heavy_minus_sign: | N/A | false | +| `Group` | **string* | :heavy_minus_sign: | N/A | | +| `EnumValues` | **string* | :heavy_minus_sign: | N/A | 1:admin only\|2:everyone | \ No newline at end of file diff --git a/docs/models/operations/statisticsmedia.md b/docs/models/operations/statisticsmedia.md new file mode 100644 index 0000000..119089f --- /dev/null +++ b/docs/models/operations/statisticsmedia.md @@ -0,0 +1,14 @@ +# StatisticsMedia + + +## Fields + +| Field | Type | Required | Description | Example | +| ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | +| `AccountID` | **int* | :heavy_minus_sign: | N/A | 1 | +| `DeviceID` | **int* | :heavy_minus_sign: | N/A | 13 | +| `Timespan` | **int* | :heavy_minus_sign: | N/A | 4 | +| `At` | **int* | :heavy_minus_sign: | N/A | 1707141600 | +| `MetadataType` | **int* | :heavy_minus_sign: | N/A | 4 | +| `Count` | **int* | :heavy_minus_sign: | N/A | 1 | +| `Duration` | **int* | :heavy_minus_sign: | N/A | 1555 | \ No newline at end of file diff --git a/docs/models/sdkerrors/getstatisticserrors.md b/docs/models/sdkerrors/getstatisticserrors.md new file mode 100644 index 0000000..149d135 --- /dev/null +++ b/docs/models/sdkerrors/getstatisticserrors.md @@ -0,0 +1,10 @@ +# GetStatisticsErrors + + +## Fields + +| Field | Type | Required | Description | Example | +| ------------------------------- | ------------------------------- | ------------------------------- | ------------------------------- | ------------------------------- | +| `Code` | **float64* | :heavy_minus_sign: | N/A | 1001 | +| `Message` | **string* | :heavy_minus_sign: | N/A | User could not be authenticated | +| `Status` | **float64* | :heavy_minus_sign: | N/A | 401 | \ No newline at end of file diff --git a/docs/models/sdkerrors/getstatisticsresponsebody.md b/docs/models/sdkerrors/getstatisticsresponsebody.md new file mode 100644 index 0000000..a13d52e --- /dev/null +++ b/docs/models/sdkerrors/getstatisticsresponsebody.md @@ -0,0 +1,11 @@ +# GetStatisticsResponseBody + +Unauthorized - Returned if the X-Plex-Token is missing from the header or query. + + +## Fields + +| Field | Type | Required | Description | +| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| `Errors` | [][sdkerrors.GetStatisticsErrors](../../models/sdkerrors/getstatisticserrors.md) | :heavy_minus_sign: | N/A | +| `RawResponse` | [*http.Response](https://pkg.go.dev/net/http#Response) | :heavy_minus_sign: | Raw HTTP response; suitable for custom response parsing | \ No newline at end of file diff --git a/docs/sdks/activities/README.md b/docs/sdks/activities/README.md index 2a3e6c7..5263b2b 100644 --- a/docs/sdks/activities/README.md +++ b/docs/sdks/activities/README.md @@ -88,7 +88,7 @@ func main() { ) - var activityUUID string = "string" + var activityUUID string = "" ctx := context.Background() res, err := s.Activities.CancelServerActivities(ctx, activityUUID) diff --git a/docs/sdks/hubs/README.md b/docs/sdks/hubs/README.md index f7f7618..a226809 100644 --- a/docs/sdks/hubs/README.md +++ b/docs/sdks/hubs/README.md @@ -34,9 +34,9 @@ func main() { ) - var count *float64 = 1262.49 + var count *float64 = plexgo.Float64(1262.49) - var onlyTransient *operations.OnlyTransient = operations.OnlyTransientOne + var onlyTransient *operations.OnlyTransient = operations.OnlyTransientOne.ToPointer() ctx := context.Background() res, err := s.Hubs.GetGlobalHubs(ctx, count, onlyTransient) @@ -93,9 +93,9 @@ func main() { var sectionID float64 = 6728.76 - var count *float64 = 9010.22 + var count *float64 = plexgo.Float64(9010.22) - var onlyTransient *operations.QueryParamOnlyTransient = operations.QueryParamOnlyTransientZero + var onlyTransient *operations.QueryParamOnlyTransient = operations.QueryParamOnlyTransientZero.ToPointer() ctx := context.Background() res, err := s.Hubs.GetLibraryHubs(ctx, sectionID, count, onlyTransient) diff --git a/docs/sdks/library/README.md b/docs/sdks/library/README.md index ba072c0..b2777d4 100644 --- a/docs/sdks/library/README.md +++ b/docs/sdks/library/README.md @@ -45,7 +45,7 @@ func main() { var url_ string = "file://C:\Image.png&type=13" - var type_ *float64 = 4462.17 + var type_ *float64 = plexgo.Float64(4462.17) ctx := context.Background() res, err := s.Library.GetFileHash(ctx, url_, type_) @@ -243,7 +243,7 @@ func main() { var sectionID float64 = 1000 - var includeDetails *operations.IncludeDetails = operations.IncludeDetailsZero + var includeDetails *operations.IncludeDetails = operations.IncludeDetailsZero.ToPointer() ctx := context.Background() res, err := s.Library.GetLibrary(ctx, sectionID, includeDetails) diff --git a/docs/sdks/media/README.md b/docs/sdks/media/README.md index a90e78e..c218685 100644 --- a/docs/sdks/media/README.md +++ b/docs/sdks/media/README.md @@ -142,11 +142,11 @@ func main() { ) - var key string = "string" + var key string = "" var time float64 = 6900.91 - var state string = "string" + var state string = "" ctx := context.Background() res, err := s.Media.UpdatePlayProgress(ctx, key, time, state) diff --git a/docs/sdks/playlists/README.md b/docs/sdks/playlists/README.md index e6b80d8..ec932a9 100644 --- a/docs/sdks/playlists/README.md +++ b/docs/sdks/playlists/README.md @@ -48,7 +48,7 @@ func main() { ctx := context.Background() res, err := s.Playlists.CreatePlaylist(ctx, operations.CreatePlaylistRequest{ - Title: "string", + Title: "", Type: operations.QueryParamTypePhoto, Smart: operations.SmartOne, URI: "https://inborn-brochure.biz", @@ -102,9 +102,9 @@ func main() { ) - var playlistType *operations.PlaylistType = operations.PlaylistTypeAudio + var playlistType *operations.PlaylistType = operations.PlaylistTypeAudio.ToPointer() - var smart *operations.QueryParamSmart = operations.QueryParamSmartZero + var smart *operations.QueryParamSmart = operations.QueryParamSmartZero.ToPointer() ctx := context.Background() res, err := s.Playlists.GetPlaylists(ctx, playlistType, smart) @@ -269,9 +269,9 @@ func main() { var playlistID float64 = 3915 - var title *string = "string" + var title *string = plexgo.String("") - var summary *string = "string" + var summary *string = plexgo.String("") ctx := context.Background() res, err := s.Playlists.UpdatePlaylist(ctx, playlistID, title, summary) @@ -444,7 +444,7 @@ func main() { var uri string = "server://12345/com.plexapp.plugins.library/library/metadata/1" - var playQueueID *float64 = 123 + var playQueueID *float64 = plexgo.Float64(123) ctx := context.Background() res, err := s.Playlists.AddPlaylistContents(ctx, playlistID, uri, playQueueID) diff --git a/docs/sdks/plex/README.md b/docs/sdks/plex/README.md index 237d67f..36749bb 100644 --- a/docs/sdks/plex/README.md +++ b/docs/sdks/plex/README.md @@ -33,9 +33,9 @@ func main() { ) - var xPlexClientIdentifier string = "string" + var xPlexClientIdentifier string = "" - var strong *bool = false + var strong *bool = plexgo.Bool(false) ctx := context.Background() res, err := s.Plex.GetPin(ctx, xPlexClientIdentifier, strong) @@ -90,9 +90,9 @@ func main() { ) - var pinID string = "string" + var pinID string = "" - var xPlexClientIdentifier string = "string" + var xPlexClientIdentifier string = "" ctx := context.Background() res, err := s.Plex.GetToken(ctx, pinID, xPlexClientIdentifier) diff --git a/docs/sdks/search/README.md b/docs/sdks/search/README.md index e2df913..412f5b6 100644 --- a/docs/sdks/search/README.md +++ b/docs/sdks/search/README.md @@ -49,9 +49,9 @@ func main() { var query string = "dylan" - var sectionID *float64 = 1516.53 + var sectionID *float64 = plexgo.Float64(1516.53) - var limit *float64 = 5 + var limit *float64 = plexgo.Float64(5) ctx := context.Background() res, err := s.Search.PerformSearch(ctx, query, sectionID, limit) @@ -112,9 +112,9 @@ func main() { var query string = "dead+poop" - var sectionID *float64 = 4094.8 + var sectionID *float64 = plexgo.Float64(4094.8) - var limit *float64 = 5 + var limit *float64 = plexgo.Float64(5) ctx := context.Background() res, err := s.Search.PerformVoiceSearch(ctx, query, sectionID, limit) diff --git a/docs/sdks/statistics/README.md b/docs/sdks/statistics/README.md new file mode 100644 index 0000000..f39f99e --- /dev/null +++ b/docs/sdks/statistics/README.md @@ -0,0 +1,63 @@ +# Statistics +(*Statistics*) + +## Overview + +API Calls that perform operations with Plex Media Server Statistics + + +### Available Operations + +* [GetStatistics](#getstatistics) - Get Media Statistics + +## GetStatistics + +This will return the media statistics for the server + +### Example Usage + +```go +package main + +import( + "github.com/LukeHagar/plexgo/models/components" + "github.com/LukeHagar/plexgo" + "context" + "log" +) + +func main() { + s := plexgo.New( + plexgo.WithSecurity(""), + ) + + + var timespan *int64 = plexgo.Int64(411769) + + ctx := context.Background() + res, err := s.Statistics.GetStatistics(ctx, timespan) + if err != nil { + log.Fatal(err) + } + + if res.Object != nil { + // handle response + } +} +``` + +### Parameters + +| Parameter | Type | Required | Description | +| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| `ctx` | [context.Context](https://pkg.go.dev/context#Context) | :heavy_check_mark: | The context to use for the request. | +| `timespan` | **int64* | :heavy_minus_sign: | The timespan to retrieve statistics for
the exact meaning of this parameter is not known
| + + +### Response + +**[*operations.GetStatisticsResponse](../../models/operations/getstatisticsresponse.md), error** +| Error Object | Status Code | Content Type | +| ----------------------------------- | ----------------------------------- | ----------------------------------- | +| sdkerrors.GetStatisticsResponseBody | 401 | application/json | +| sdkerrors.SDKError | 4xx-5xx | */* | diff --git a/docs/sdks/updater/README.md b/docs/sdks/updater/README.md index b42b1c7..1da8468 100644 --- a/docs/sdks/updater/README.md +++ b/docs/sdks/updater/README.md @@ -85,7 +85,7 @@ func main() { ) - var download *operations.Download = operations.DownloadOne + var download *operations.Download = operations.DownloadOne.ToPointer() ctx := context.Background() res, err := s.Updater.CheckForUpdates(ctx, download) @@ -140,9 +140,9 @@ func main() { ) - var tonight *operations.Tonight = operations.TonightOne + var tonight *operations.Tonight = operations.TonightOne.ToPointer() - var skip *operations.Skip = operations.SkipZero + var skip *operations.Skip = operations.SkipZero.ToPointer() ctx := context.Background() res, err := s.Updater.ApplyUpdates(ctx, tonight, skip) diff --git a/docs/sdks/video/README.md b/docs/sdks/video/README.md index 7ca81e4..0aee031 100644 --- a/docs/sdks/video/README.md +++ b/docs/sdks/video/README.md @@ -8,65 +8,8 @@ API Calls that perform operations with Plex Media Server Videos ### Available Operations -* [StartUniversalTranscode](#startuniversaltranscode) - Start Universal Transcode * [GetTimeline](#gettimeline) - Get the timeline for a media item - -## StartUniversalTranscode - -Begin a Universal Transcode Session - -### Example Usage - -```go -package main - -import( - "github.com/LukeHagar/plexgo/models/components" - "github.com/LukeHagar/plexgo" - "context" - "github.com/LukeHagar/plexgo/models/operations" - "log" - "net/http" -) - -func main() { - s := plexgo.New( - plexgo.WithSecurity(""), - ) - - ctx := context.Background() - res, err := s.Video.StartUniversalTranscode(ctx, operations.StartUniversalTranscodeRequest{ - HasMDE: 8924.99, - Path: "/etc/mail", - MediaIndex: 9962.95, - PartIndex: 1232.82, - Protocol: "string", - }) - if err != nil { - log.Fatal(err) - } - - if res.StatusCode == http.StatusOK { - // handle response - } -} -``` - -### Parameters - -| Parameter | Type | Required | Description | -| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | -| `ctx` | [context.Context](https://pkg.go.dev/context#Context) | :heavy_check_mark: | The context to use for the request. | -| `request` | [operations.StartUniversalTranscodeRequest](../../models/operations/startuniversaltranscoderequest.md) | :heavy_check_mark: | The request object to use for the request. | - - -### Response - -**[*operations.StartUniversalTranscodeResponse](../../models/operations/startuniversaltranscoderesponse.md), error** -| Error Object | Status Code | Content Type | -| --------------------------------------------- | --------------------------------------------- | --------------------------------------------- | -| sdkerrors.StartUniversalTranscodeResponseBody | 401 | application/json | -| sdkerrors.SDKError | 4xx-5xx | */* | +* [StartUniversalTranscode](#startuniversaltranscode) - Start Universal Transcode ## GetTimeline @@ -99,7 +42,7 @@ func main() { HasMDE: 7574.33, Time: 3327.51, Duration: 7585.39, - Context: "string", + Context: "", PlayQueueItemID: 1406.21, PlayBackTime: 2699.34, Row: 3536.42, @@ -129,3 +72,60 @@ func main() { | --------------------------------- | --------------------------------- | --------------------------------- | | sdkerrors.GetTimelineResponseBody | 401 | application/json | | sdkerrors.SDKError | 4xx-5xx | */* | + +## StartUniversalTranscode + +Begin a Universal Transcode Session + +### Example Usage + +```go +package main + +import( + "github.com/LukeHagar/plexgo/models/components" + "github.com/LukeHagar/plexgo" + "context" + "github.com/LukeHagar/plexgo/models/operations" + "log" + "net/http" +) + +func main() { + s := plexgo.New( + plexgo.WithSecurity(""), + ) + + ctx := context.Background() + res, err := s.Video.StartUniversalTranscode(ctx, operations.StartUniversalTranscodeRequest{ + HasMDE: 8924.99, + Path: "/etc/mail", + MediaIndex: 9962.95, + PartIndex: 1232.82, + Protocol: "", + }) + if err != nil { + log.Fatal(err) + } + + if res.StatusCode == http.StatusOK { + // handle response + } +} +``` + +### Parameters + +| Parameter | Type | Required | Description | +| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | +| `ctx` | [context.Context](https://pkg.go.dev/context#Context) | :heavy_check_mark: | The context to use for the request. | +| `request` | [operations.StartUniversalTranscodeRequest](../../models/operations/startuniversaltranscoderequest.md) | :heavy_check_mark: | The request object to use for the request. | + + +### Response + +**[*operations.StartUniversalTranscodeResponse](../../models/operations/startuniversaltranscoderesponse.md), error** +| Error Object | Status Code | Content Type | +| --------------------------------------------- | --------------------------------------------- | --------------------------------------------- | +| sdkerrors.StartUniversalTranscodeResponseBody | 401 | application/json | +| sdkerrors.SDKError | 4xx-5xx | */* | diff --git a/gen.yaml b/gen.yaml index 4b35bcb..5210266 100644 --- a/gen.yaml +++ b/gen.yaml @@ -8,8 +8,10 @@ generation: fixes: nameResolutionDec2023: false parameterOrderingFeb2024: false + requestResponseComponentNamesFeb2024: false go: - version: 0.3.0 + version: 0.4.0 + additionalDependencies: {} clientServerStatusCodesAsErrors: true flattenGlobalSecurity: true imports: diff --git a/go.mod b/go.mod index 5ad342e..856c657 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.20 require ( github.com/cenkalti/backoff/v4 v4.2.0 - github.com/ericlagergren/decimal v0.0.0-20221120152707-495c53812d05 - github.com/mitchellh/mapstructure v1.5.0 - github.com/spyzhov/ajson v0.8.0 + github.com/ericlagergren/decimal v0.0.0-20221120152707-495c53812d05 + github.com/spyzhov/ajson v0.8.0 ) diff --git a/go.sum b/go.sum deleted file mode 100644 index ae03a47..0000000 --- a/go.sum +++ /dev/null @@ -1,8 +0,0 @@ -github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= -github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/ericlagergren/decimal v0.0.0-20221120152707-495c53812d05 h1:S92OBrGuLLZsyM5ybUzgc/mPjIYk2AZqufieooe98uw= -github.com/ericlagergren/decimal v0.0.0-20221120152707-495c53812d05/go.mod h1:M9R1FoZ3y//hwwnJtO51ypFGwm8ZfpxPT/ZLtO1mcgQ= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/spyzhov/ajson v0.8.0 h1:sFXyMbi4Y/BKjrsfkUZHSjA2JM1184enheSjjoT/zCc= -github.com/spyzhov/ajson v0.8.0/go.mod h1:63V+CGM6f1Bu/p4nLIN8885ojBdt88TbLoSFzyqMuVA= diff --git a/hubs.go b/hubs.go index 2e0da3b..e8778ca 100644 --- a/hubs.go +++ b/hubs.go @@ -6,12 +6,13 @@ import ( "bytes" "context" "fmt" + "github.com/LukeHagar/plexgo/internal/hooks" "github.com/LukeHagar/plexgo/internal/utils" "github.com/LukeHagar/plexgo/models/operations" "github.com/LukeHagar/plexgo/models/sdkerrors" "io" "net/http" - "strings" + "net/url" ) // Hubs are a structured two-dimensional container for media, generally represented by multiple horizontal rows. @@ -28,35 +29,58 @@ func newHubs(sdkConfig sdkConfiguration) *Hubs { // GetGlobalHubs - Get Global Hubs // Get Global Hubs filtered by the parameters provided. func (s *Hubs) GetGlobalHubs(ctx context.Context, count *float64, onlyTransient *operations.OnlyTransient) (*operations.GetGlobalHubsResponse, error) { + hookCtx := hooks.HookContext{OperationID: "getGlobalHubs"} + request := operations.GetGlobalHubsRequest{ Count: count, OnlyTransient: onlyTransient, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/hubs" + opURL, err := url.JoinPath(baseURL, "/hubs") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetGlobalHubsResponse{ @@ -71,6 +95,7 @@ func (s *Hubs) GetGlobalHubs(ctx context.Context, count *float64, onlyTransient } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -111,6 +136,8 @@ func (s *Hubs) GetGlobalHubs(ctx context.Context, count *float64, onlyTransient // GetLibraryHubs - Get library specific hubs // This endpoint will return a list of library specific hubs func (s *Hubs) GetLibraryHubs(ctx context.Context, sectionID float64, count *float64, onlyTransient *operations.QueryParamOnlyTransient) (*operations.GetLibraryHubsResponse, error) { + hookCtx := hooks.HookContext{OperationID: "getLibraryHubs"} + request := operations.GetLibraryHubsRequest{ SectionID: sectionID, Count: count, @@ -118,32 +145,50 @@ func (s *Hubs) GetLibraryHubs(ctx context.Context, sectionID float64, count *flo } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url, err := utils.GenerateURL(ctx, baseURL, "/hubs/sections/{sectionId}", request, nil) + opURL, err := utils.GenerateURL(ctx, baseURL, "/hubs/sections/{sectionId}", request, nil) if err != nil { return nil, fmt.Errorf("error generating URL: %w", err) } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetLibraryHubsResponse{ @@ -158,6 +203,7 @@ func (s *Hubs) GetLibraryHubs(ctx context.Context, sectionID float64, count *flo } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { diff --git a/internal/hooks/hooks.go b/internal/hooks/hooks.go new file mode 100644 index 0000000..f25bdb0 --- /dev/null +++ b/internal/hooks/hooks.go @@ -0,0 +1,140 @@ +// Code generated by Speakeasy (https://speakeasyapi.dev). DO NOT EDIT. + +package hooks + +import ( + "errors" + "net/http" +) + +type FailEarly struct { + Cause error +} + +var _ error = (*FailEarly)(nil) + +func (f *FailEarly) Error() string { + return f.Cause.Error() +} + +// HTTPClient provides an interface for supplying the SDK with a custom HTTP client +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +type HookContext struct { + OperationID string +} + +type BeforeRequestContext struct { + HookContext +} + +type AfterSuccessContext struct { + HookContext +} + +type AfterErrorContext struct { + HookContext +} + +// clientInitHook is called when the SDK is initializing the HTTP client. The hook can return a new HTTP client to be used by the SDK. +type clientInitHook interface { + ClientInit(client HTTPClient) HTTPClient +} + +// beforeRequestHook is called before the SDK sends a request. The hook can modify the request before it is sent or return an error to stop the request from being sent. +type beforeRequestHook interface { + BeforeRequest(hookCtx BeforeRequestContext, req *http.Request) (*http.Request, error) +} + +// afterSuccessHook is called after the SDK receives a response. The hook can modify the response before it is handled or return an error to stop the response from being handled. +type afterSuccessHook interface { + AfterSuccess(hookCtx AfterSuccessContext, res *http.Response) (*http.Response, error) +} + +// afterErrorHook is called after the SDK encounters an error, or a non-successful response. The hook can modify the response if available otherwise modify the error. +// All afterErrorHook hooks are called and returning an error won't stop the other hooks from being called. But if you want to stop the other hooks from being called, you can return a FailEarly error wrapping your error. +type afterErrorHook interface { + AfterError(hookCtx AfterErrorContext, res *http.Response, err error) (*http.Response, error) +} + +type Hooks struct { + clientInitHooks []clientInitHook + beforeRequestHook []beforeRequestHook + afterSuccessHook []afterSuccessHook + afterErrorHook []afterErrorHook +} + +func New() *Hooks { + h := &Hooks{ + clientInitHooks: []clientInitHook{}, + beforeRequestHook: []beforeRequestHook{}, + afterSuccessHook: []afterSuccessHook{}, + afterErrorHook: []afterErrorHook{}, + } + + initHooks(h) + + return h +} + +// registerClientInitHook registers a hook to be used by the SDK for the client initialization event. +func (h *Hooks) registerClientInitHook(hook clientInitHook) { + h.clientInitHooks = append(h.clientInitHooks, hook) +} + +// registerBeforeRequestHook registers a hook to be used by the SDK for the before request event. +func (h *Hooks) registerBeforeRequestHook(hook beforeRequestHook) { + h.beforeRequestHook = append(h.beforeRequestHook, hook) +} + +// registerAfterSuccessHook registers a hook to be used by the SDK for the after success event. +func (h *Hooks) registerAfterSuccessHook(hook afterSuccessHook) { + h.afterSuccessHook = append(h.afterSuccessHook, hook) +} + +// registerAfterErrorHook registers a hook to be used by the SDK for the after error event. +func (h *Hooks) registerAfterErrorHook(hook afterErrorHook) { + h.afterErrorHook = append(h.afterErrorHook, hook) +} + +func (h *Hooks) ClientInit(client HTTPClient) HTTPClient { + for _, hook := range h.clientInitHooks { + client = hook.ClientInit(client) + } + return client +} + +func (h *Hooks) BeforeRequest(hookCtx BeforeRequestContext, req *http.Request) (*http.Request, error) { + for _, hook := range h.beforeRequestHook { + var err error + req, err = hook.BeforeRequest(hookCtx, req) + if err != nil { + return req, err + } + } + return req, nil +} + +func (h *Hooks) AfterSuccess(hookCtx AfterSuccessContext, res *http.Response) (*http.Response, error) { + for _, hook := range h.afterSuccessHook { + var err error + res, err = hook.AfterSuccess(hookCtx, res) + if err != nil { + return res, err + } + } + return res, nil +} + +func (h *Hooks) AfterError(hookCtx AfterErrorContext, res *http.Response, err error) (*http.Response, error) { + for _, hook := range h.afterErrorHook { + res, err = hook.AfterError(hookCtx, res, err) + var fe *FailEarly + if errors.As(err, &fe) { + return nil, fe.Cause + } + } + return res, err +} diff --git a/internal/hooks/registration.go b/internal/hooks/registration.go new file mode 100644 index 0000000..bc56b9b --- /dev/null +++ b/internal/hooks/registration.go @@ -0,0 +1,13 @@ +package hooks + +/* + * This file is only ever generated once on the first generation and then is free to be modified. + * Any hooks you wish to add should be registered in the InitHooks function. Feel free to define them + * in this file or in separate files in the hooks package. + */ + +func initHooks(h *Hooks) { + // Add hooks by calling h.register{ClientInit/BeforeRequest/AfterRequest/AfterError}Hook + // with an instance of a hook that implements that specific Hook interface + // Hooks are registered per SDK instance, and are valid for the lifetime of the SDK instance +} diff --git a/internal/utils/json.go b/internal/utils/json.go index fcd3775..3b905ca 100644 --- a/internal/utils/json.go +++ b/internal/utils/json.go @@ -358,7 +358,7 @@ func handleDefaultConstValue(tagValue string, val interface{}, tag reflect.Struc return []byte(fmt.Sprintf(`"%s"`, tagValue)) default: if typ.Kind() == reflect.String { - return []byte(fmt.Sprintf(`"%s"`, tagValue)) + return []byte(fmt.Sprintf("%q", tagValue)) } } diff --git a/internal/utils/requestbody.go b/internal/utils/requestbody.go index 950f39a..322b8b3 100644 --- a/internal/utils/requestbody.go +++ b/internal/utils/requestbody.go @@ -26,6 +26,19 @@ var ( ) func SerializeRequestBody(ctx context.Context, request interface{}, nullable, optional bool, requestFieldName, serializationMethod, tag string) (io.Reader, string, error) { + bodyReader, contentType, err := serializeRequestBody(ctx, request, nullable, optional, requestFieldName, serializationMethod, tag) + if err != nil { + return nil, "", fmt.Errorf("error serializing request body: %w", err) + } + + if bodyReader == nil && !optional { + return nil, "", fmt.Errorf("request body is required") + } + + return bodyReader, contentType, nil +} + +func serializeRequestBody(ctx context.Context, request interface{}, nullable, optional bool, requestFieldName, serializationMethod, tag string) (io.Reader, string, error) { requestStructType := reflect.TypeOf(request) requestValType := reflect.ValueOf(request) diff --git a/internal/utils/utils.go b/internal/utils/utils.go index b4618bb..5508c5f 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -8,6 +8,7 @@ import ( "math/big" "reflect" "regexp" + "strconv" "strings" "time" @@ -63,6 +64,29 @@ func Contains(slice []string, item string) bool { return false } +func MatchStatusCodes(expectedCodes []string, statusCode int) bool { + for _, codeStr := range expectedCodes { + code, err := strconv.Atoi(codeStr) + if err == nil { + if code == statusCode { + return true + } + continue + } + + codeRange, err := strconv.Atoi(string(codeStr[0])) + if err != nil { + continue + } + + if statusCode >= (codeRange*100) && statusCode < ((codeRange+1)*100) { + return true + } + } + + return false +} + func parseStructTag(tagKey string, field reflect.StructField) map[string]string { tag := field.Tag.Get(tagKey) if tag == "" { diff --git a/library.go b/library.go index e54da54..02460ac 100644 --- a/library.go +++ b/library.go @@ -6,12 +6,13 @@ import ( "bytes" "context" "fmt" + "github.com/LukeHagar/plexgo/internal/hooks" "github.com/LukeHagar/plexgo/internal/utils" "github.com/LukeHagar/plexgo/models/operations" "github.com/LukeHagar/plexgo/models/sdkerrors" "io" "net/http" - "strings" + "net/url" ) // Library - API Calls interacting with Plex Media Server Libraries @@ -28,35 +29,58 @@ func newLibrary(sdkConfig sdkConfiguration) *Library { // GetFileHash - Get Hash Value // This resource returns hash values for local files func (s *Library) GetFileHash(ctx context.Context, url_ string, type_ *float64) (*operations.GetFileHashResponse, error) { + hookCtx := hooks.HookContext{OperationID: "getFileHash"} + request := operations.GetFileHashRequest{ URL: url_, Type: type_, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/library/hashes" + opURL, err := url.JoinPath(baseURL, "/library/hashes") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetFileHashResponse{ @@ -71,6 +95,7 @@ func (s *Library) GetFileHash(ctx context.Context, url_ string, type_ *float64) } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: @@ -100,26 +125,49 @@ func (s *Library) GetFileHash(ctx context.Context, url_ string, type_ *float64) // GetRecentlyAdded - Get Recently Added // This endpoint will return the recently added content. func (s *Library) GetRecentlyAdded(ctx context.Context) (*operations.GetRecentlyAddedResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/library/recentlyAdded" + hookCtx := hooks.HookContext{OperationID: "getRecentlyAdded"} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/library/recentlyAdded") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetRecentlyAddedResponse{ @@ -134,6 +182,7 @@ func (s *Library) GetRecentlyAdded(ctx context.Context) (*operations.GetRecently } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -179,26 +228,49 @@ func (s *Library) GetRecentlyAdded(ctx context.Context) (*operations.GetRecently // 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). func (s *Library) GetLibraries(ctx context.Context) (*operations.GetLibrariesResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/library/sections" + hookCtx := hooks.HookContext{OperationID: "getLibraries"} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/library/sections") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetLibrariesResponse{ @@ -213,6 +285,7 @@ func (s *Library) GetLibraries(ctx context.Context) (*operations.GetLibrariesRes } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -291,38 +364,58 @@ func (s *Library) GetLibraries(ctx context.Context) (*operations.GetLibrariesRes // // > **Note**: Filters and sorts are optional; without them, no filtering controls are rendered. func (s *Library) GetLibrary(ctx context.Context, sectionID float64, includeDetails *operations.IncludeDetails) (*operations.GetLibraryResponse, error) { + hookCtx := hooks.HookContext{OperationID: "getLibrary"} + request := operations.GetLibraryRequest{ SectionID: sectionID, IncludeDetails: includeDetails, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url, err := utils.GenerateURL(ctx, baseURL, "/library/sections/{sectionId}", request, nil) + opURL, err := utils.GenerateURL(ctx, baseURL, "/library/sections/{sectionId}", request, nil) if err != nil { return nil, fmt.Errorf("error generating URL: %w", err) } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetLibraryResponse{ @@ -337,6 +430,7 @@ func (s *Library) GetLibrary(ctx context.Context, sectionID float64, includeDeta } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -377,33 +471,53 @@ func (s *Library) GetLibrary(ctx context.Context, sectionID float64, includeDeta // DeleteLibrary - Delete Library Section // Delate a library using a specific section func (s *Library) DeleteLibrary(ctx context.Context, sectionID float64) (*operations.DeleteLibraryResponse, error) { + hookCtx := hooks.HookContext{OperationID: "deleteLibrary"} + request := operations.DeleteLibraryRequest{ SectionID: sectionID, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url, err := utils.GenerateURL(ctx, baseURL, "/library/sections/{sectionId}", request, nil) + opURL, err := utils.GenerateURL(ctx, baseURL, "/library/sections/{sectionId}", request, nil) if err != nil { return nil, fmt.Errorf("error generating URL: %w", err) } - req, err := http.NewRequestWithContext(ctx, "DELETE", url, nil) + req, err := http.NewRequestWithContext(ctx, "DELETE", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.DeleteLibraryResponse{ @@ -418,6 +532,7 @@ func (s *Library) DeleteLibrary(ctx context.Context, sectionID float64) (*operat } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: @@ -466,34 +581,54 @@ func (s *Library) DeleteLibrary(ctx context.Context, sectionID float64) (*operat // - `firstCharacter`: Items categorized by the first letter. // - `folder`: Items categorized by folder. func (s *Library) GetLibraryItems(ctx context.Context, sectionID int64, tag operations.Tag) (*operations.GetLibraryItemsResponse, error) { + hookCtx := hooks.HookContext{OperationID: "getLibraryItems"} + request := operations.GetLibraryItemsRequest{ SectionID: sectionID, Tag: tag, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url, err := utils.GenerateURL(ctx, baseURL, "/library/sections/{sectionId}/{tag}", request, nil) + opURL, err := utils.GenerateURL(ctx, baseURL, "/library/sections/{sectionId}/{tag}", request, nil) if err != nil { return nil, fmt.Errorf("error generating URL: %w", err) } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetLibraryItemsResponse{ @@ -508,6 +643,7 @@ func (s *Library) GetLibraryItems(ctx context.Context, sectionID int64, tag oper } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -533,33 +669,53 @@ func (s *Library) GetLibraryItems(ctx context.Context, sectionID int64, tag oper // RefreshLibrary - Refresh Library // This endpoint Refreshes the library. func (s *Library) RefreshLibrary(ctx context.Context, sectionID float64) (*operations.RefreshLibraryResponse, error) { + hookCtx := hooks.HookContext{OperationID: "refreshLibrary"} + request := operations.RefreshLibraryRequest{ SectionID: sectionID, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url, err := utils.GenerateURL(ctx, baseURL, "/library/sections/{sectionId}/refresh", request, nil) + opURL, err := utils.GenerateURL(ctx, baseURL, "/library/sections/{sectionId}/refresh", request, nil) if err != nil { return nil, fmt.Errorf("error generating URL: %w", err) } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.RefreshLibraryResponse{ @@ -574,6 +730,7 @@ func (s *Library) RefreshLibrary(ctx context.Context, sectionID float64) (*opera } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: @@ -620,38 +777,58 @@ func (s *Library) RefreshLibrary(ctx context.Context, sectionID float64) (*opera // // > **Note**: Filters and sorts are optional; without them, no filtering controls are rendered. func (s *Library) SearchLibrary(ctx context.Context, sectionID int64, type_ operations.Type) (*operations.SearchLibraryResponse, error) { + hookCtx := hooks.HookContext{OperationID: "searchLibrary"} + request := operations.SearchLibraryRequest{ SectionID: sectionID, Type: type_, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url, err := utils.GenerateURL(ctx, baseURL, "/library/sections/{sectionId}/search", request, nil) + opURL, err := utils.GenerateURL(ctx, baseURL, "/library/sections/{sectionId}/search", request, nil) if err != nil { return nil, fmt.Errorf("error generating URL: %w", err) } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.SearchLibraryResponse{ @@ -666,6 +843,7 @@ func (s *Library) SearchLibrary(ctx context.Context, sectionID int64, type_ oper } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -691,33 +869,53 @@ func (s *Library) SearchLibrary(ctx context.Context, sectionID int64, type_ oper // GetMetadata - Get Items Metadata // This endpoint will return the metadata of a library item specified with the ratingKey. func (s *Library) GetMetadata(ctx context.Context, ratingKey float64) (*operations.GetMetadataResponse, error) { + hookCtx := hooks.HookContext{OperationID: "getMetadata"} + request := operations.GetMetadataRequest{ RatingKey: ratingKey, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url, err := utils.GenerateURL(ctx, baseURL, "/library/metadata/{ratingKey}", request, nil) + opURL, err := utils.GenerateURL(ctx, baseURL, "/library/metadata/{ratingKey}", request, nil) if err != nil { return nil, fmt.Errorf("error generating URL: %w", err) } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetMetadataResponse{ @@ -732,6 +930,7 @@ func (s *Library) GetMetadata(ctx context.Context, ratingKey float64) (*operatio } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -772,33 +971,53 @@ func (s *Library) GetMetadata(ctx context.Context, ratingKey float64) (*operatio // GetMetadataChildren - Get Items Children // This endpoint will return the children of of a library item specified with the ratingKey. func (s *Library) GetMetadataChildren(ctx context.Context, ratingKey float64) (*operations.GetMetadataChildrenResponse, error) { + hookCtx := hooks.HookContext{OperationID: "getMetadataChildren"} + request := operations.GetMetadataChildrenRequest{ RatingKey: ratingKey, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url, err := utils.GenerateURL(ctx, baseURL, "/library/metadata/{ratingKey}/children", request, nil) + opURL, err := utils.GenerateURL(ctx, baseURL, "/library/metadata/{ratingKey}/children", request, nil) if err != nil { return nil, fmt.Errorf("error generating URL: %w", err) } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetMetadataChildrenResponse{ @@ -813,6 +1032,7 @@ func (s *Library) GetMetadataChildren(ctx context.Context, ratingKey float64) (* } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -853,26 +1073,49 @@ func (s *Library) GetMetadataChildren(ctx context.Context, ratingKey float64) (* // GetOnDeck - Get On Deck // This endpoint will return the on deck content. func (s *Library) GetOnDeck(ctx context.Context) (*operations.GetOnDeckResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/library/onDeck" + hookCtx := hooks.HookContext{OperationID: "getOnDeck"} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/library/onDeck") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetOnDeckResponse{ @@ -887,6 +1130,7 @@ func (s *Library) GetOnDeck(ctx context.Context) (*operations.GetOnDeckResponse, } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { diff --git a/log.go b/log.go index c328463..861bfde 100644 --- a/log.go +++ b/log.go @@ -6,12 +6,13 @@ import ( "bytes" "context" "fmt" + "github.com/LukeHagar/plexgo/internal/hooks" "github.com/LukeHagar/plexgo/internal/utils" "github.com/LukeHagar/plexgo/models/operations" "github.com/LukeHagar/plexgo/models/sdkerrors" "io" "net/http" - "strings" + "net/url" ) // Log - Submit logs to the Log Handler for Plex Media Server @@ -28,6 +29,8 @@ func newLog(sdkConfig sdkConfiguration) *Log { // LogLine - Logging a single line message. // This endpoint will write a single-line log message, including a level and source to the main Plex Media Server log. func (s *Log) LogLine(ctx context.Context, level operations.Level, message string, source string) (*operations.LogLineResponse, error) { + hookCtx := hooks.HookContext{OperationID: "logLine"} + request := operations.LogLineRequest{ Level: level, Message: message, @@ -35,29 +38,50 @@ func (s *Log) LogLine(ctx context.Context, level operations.Level, message strin } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/log" + opURL, err := url.JoinPath(baseURL, "/log") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.LogLineResponse{ @@ -72,6 +96,7 @@ func (s *Log) LogLine(ctx context.Context, level operations.Level, message strin } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: @@ -121,36 +146,55 @@ func (s *Log) LogLine(ctx context.Context, level operations.Level, message strin // // Ensure each parameter is properly URL-encoded to avoid interpretation issues. func (s *Log) LogMultiLine(ctx context.Context, request string) (*operations.LogMultiLineResponse, error) { + hookCtx := hooks.HookContext{OperationID: "logMultiLine"} + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/log" + opURL, err := url.JoinPath(baseURL, "/log") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, false, "Request", "string", `request:"mediaType=text/plain"`) if err != nil { - return nil, fmt.Errorf("error serializing request body: %w", err) - } - if bodyReader == nil { - return nil, fmt.Errorf("request body is required") + return nil, err } - req, err := http.NewRequestWithContext(ctx, "POST", url, bodyReader) + req, err := http.NewRequestWithContext(ctx, "POST", opURL, bodyReader) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) - + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) req.Header.Set("Content-Type", reqContentType) + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.LogMultiLineResponse{ @@ -165,6 +209,7 @@ func (s *Log) LogMultiLine(ctx context.Context, request string) (*operations.Log } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: @@ -194,26 +239,49 @@ func (s *Log) LogMultiLine(ctx context.Context, request string) (*operations.Log // EnablePaperTrail - Enabling Papertrail // This endpoint will enable all Plex Media Serverlogs to be sent to the Papertrail networked logging site for a period of time. func (s *Log) EnablePaperTrail(ctx context.Context) (*operations.EnablePaperTrailResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/log/networked" + hookCtx := hooks.HookContext{OperationID: "enablePaperTrail"} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/log/networked") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "403", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.EnablePaperTrailResponse{ @@ -228,6 +296,7 @@ func (s *Log) EnablePaperTrail(ctx context.Context) (*operations.EnablePaperTrai } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: diff --git a/media.go b/media.go index 2c41e43..43b8313 100644 --- a/media.go +++ b/media.go @@ -6,12 +6,13 @@ import ( "bytes" "context" "fmt" + "github.com/LukeHagar/plexgo/internal/hooks" "github.com/LukeHagar/plexgo/internal/utils" "github.com/LukeHagar/plexgo/models/operations" "github.com/LukeHagar/plexgo/models/sdkerrors" "io" "net/http" - "strings" + "net/url" ) // Media - API Calls interacting with Plex Media Server Media @@ -28,34 +29,57 @@ func newMedia(sdkConfig sdkConfiguration) *Media { // MarkPlayed - Mark Media Played // This will mark the provided media key as Played. func (s *Media) MarkPlayed(ctx context.Context, key float64) (*operations.MarkPlayedResponse, error) { + hookCtx := hooks.HookContext{OperationID: "markPlayed"} + request := operations.MarkPlayedRequest{ Key: key, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/:/scrobble" + opURL, err := url.JoinPath(baseURL, "/:/scrobble") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.MarkPlayedResponse{ @@ -70,6 +94,7 @@ func (s *Media) MarkPlayed(ctx context.Context, key float64) (*operations.MarkPl } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: @@ -99,34 +124,57 @@ func (s *Media) MarkPlayed(ctx context.Context, key float64) (*operations.MarkPl // MarkUnplayed - Mark Media Unplayed // This will mark the provided media key as Unplayed. func (s *Media) MarkUnplayed(ctx context.Context, key float64) (*operations.MarkUnplayedResponse, error) { + hookCtx := hooks.HookContext{OperationID: "markUnplayed"} + request := operations.MarkUnplayedRequest{ Key: key, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/:/unscrobble" + opURL, err := url.JoinPath(baseURL, "/:/unscrobble") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.MarkUnplayedResponse{ @@ -141,6 +189,7 @@ func (s *Media) MarkUnplayed(ctx context.Context, key float64) (*operations.Mark } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: @@ -170,6 +219,8 @@ func (s *Media) MarkUnplayed(ctx context.Context, key float64) (*operations.Mark // UpdatePlayProgress - Update Media Play Progress // This API command can be used to update the play progress of a media item. func (s *Media) UpdatePlayProgress(ctx context.Context, key string, time float64, state string) (*operations.UpdatePlayProgressResponse, error) { + hookCtx := hooks.HookContext{OperationID: "updatePlayProgress"} + request := operations.UpdatePlayProgressRequest{ Key: key, Time: time, @@ -177,29 +228,50 @@ func (s *Media) UpdatePlayProgress(ctx context.Context, key string, time float64 } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/:/progress" + opURL, err := url.JoinPath(baseURL, "/:/progress") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } - req, err := http.NewRequestWithContext(ctx, "POST", url, nil) + req, err := http.NewRequestWithContext(ctx, "POST", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.UpdatePlayProgressResponse{ @@ -214,6 +286,7 @@ func (s *Media) UpdatePlayProgress(ctx context.Context, key string, time float64 } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: diff --git a/models/operations/getserverpreferences.go b/models/operations/getserverpreferences.go index 8f62a72..5a6f4e3 100644 --- a/models/operations/getserverpreferences.go +++ b/models/operations/getserverpreferences.go @@ -3,232 +3,92 @@ package operations import ( - "errors" - "github.com/LukeHagar/plexgo/internal/utils" "net/http" ) -type Two struct { +type Setting struct { ID *string `json:"id,omitempty"` Label *string `json:"label,omitempty"` Summary *string `json:"summary,omitempty"` Type *string `json:"type,omitempty"` - Default *int `json:"default,omitempty"` - Value *int `json:"value,omitempty"` + Default *bool `json:"default,omitempty"` + Value *bool `json:"value,omitempty"` Hidden *bool `json:"hidden,omitempty"` Advanced *bool `json:"advanced,omitempty"` Group *string `json:"group,omitempty"` EnumValues *string `json:"enumValues,omitempty"` } -func (o *Two) GetID() *string { +func (o *Setting) GetID() *string { if o == nil { return nil } return o.ID } -func (o *Two) GetLabel() *string { +func (o *Setting) GetLabel() *string { if o == nil { return nil } return o.Label } -func (o *Two) GetSummary() *string { +func (o *Setting) GetSummary() *string { if o == nil { return nil } return o.Summary } -func (o *Two) GetType() *string { +func (o *Setting) GetType() *string { if o == nil { return nil } return o.Type } -func (o *Two) GetDefault() *int { +func (o *Setting) GetDefault() *bool { if o == nil { return nil } return o.Default } -func (o *Two) GetValue() *int { +func (o *Setting) GetValue() *bool { if o == nil { return nil } return o.Value } -func (o *Two) GetHidden() *bool { +func (o *Setting) GetHidden() *bool { if o == nil { return nil } return o.Hidden } -func (o *Two) GetAdvanced() *bool { +func (o *Setting) GetAdvanced() *bool { if o == nil { return nil } return o.Advanced } -func (o *Two) GetGroup() *string { +func (o *Setting) GetGroup() *string { if o == nil { return nil } return o.Group } -func (o *Two) GetEnumValues() *string { +func (o *Setting) GetEnumValues() *string { if o == nil { return nil } return o.EnumValues } -type One struct { - ID *string `json:"id,omitempty"` - Label *string `json:"label,omitempty"` - Summary *string `json:"summary,omitempty"` - Type *string `json:"type,omitempty"` - Default *string `json:"default,omitempty"` - Value *string `json:"value,omitempty"` - Hidden *bool `json:"hidden,omitempty"` - Advanced *bool `json:"advanced,omitempty"` - Group *string `json:"group,omitempty"` -} - -func (o *One) GetID() *string { - if o == nil { - return nil - } - return o.ID -} - -func (o *One) GetLabel() *string { - if o == nil { - return nil - } - return o.Label -} - -func (o *One) GetSummary() *string { - if o == nil { - return nil - } - return o.Summary -} - -func (o *One) GetType() *string { - if o == nil { - return nil - } - return o.Type -} - -func (o *One) GetDefault() *string { - if o == nil { - return nil - } - return o.Default -} - -func (o *One) GetValue() *string { - if o == nil { - return nil - } - return o.Value -} - -func (o *One) GetHidden() *bool { - if o == nil { - return nil - } - return o.Hidden -} - -func (o *One) GetAdvanced() *bool { - if o == nil { - return nil - } - return o.Advanced -} - -func (o *One) GetGroup() *string { - if o == nil { - return nil - } - return o.Group -} - -type SettingType string - -const ( - SettingTypeOne SettingType = "1" - SettingTypeTwo SettingType = "2" -) - -type Setting struct { - One *One - Two *Two - - Type SettingType -} - -func CreateSettingOne(one One) Setting { - typ := SettingTypeOne - - return Setting{ - One: &one, - Type: typ, - } -} - -func CreateSettingTwo(two Two) Setting { - typ := SettingTypeTwo - - return Setting{ - Two: &two, - Type: typ, - } -} - -func (u *Setting) UnmarshalJSON(data []byte) error { - - one := One{} - if err := utils.UnmarshalJSON(data, &one, "", true, true); err == nil { - u.One = &one - u.Type = SettingTypeOne - return nil - } - - two := Two{} - if err := utils.UnmarshalJSON(data, &two, "", true, true); err == nil { - u.Two = &two - u.Type = SettingTypeTwo - return nil - } - - return errors.New("could not unmarshal into supported union types") -} - -func (u Setting) MarshalJSON() ([]byte, error) { - if u.One != nil { - return utils.MarshalJSON(u.One, "", true) - } - - if u.Two != nil { - return utils.MarshalJSON(u.Two, "", true) - } - - return nil, errors.New("could not marshal union type: all fields are null") -} - type GetServerPreferencesMediaContainer struct { Size *int `json:"size,omitempty"` Setting []Setting `json:"Setting,omitempty"` diff --git a/models/operations/getstatistics.go b/models/operations/getstatistics.go new file mode 100644 index 0000000..2f70864 --- /dev/null +++ b/models/operations/getstatistics.go @@ -0,0 +1,276 @@ +// Code generated by Speakeasy (https://speakeasyapi.dev). DO NOT EDIT. + +package operations + +import ( + "net/http" +) + +type GetStatisticsRequest struct { + // The timespan to retrieve statistics for + // the exact meaning of this parameter is not known + // + Timespan *int64 `queryParam:"style=form,explode=true,name=Timespan"` +} + +func (o *GetStatisticsRequest) GetTimespan() *int64 { + if o == nil { + return nil + } + return o.Timespan +} + +type GetStatisticsDevice struct { + ID *int `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Platform *string `json:"platform,omitempty"` + ClientIdentifier *string `json:"clientIdentifier,omitempty"` + CreatedAt *int `json:"createdAt,omitempty"` +} + +func (o *GetStatisticsDevice) GetID() *int { + if o == nil { + return nil + } + return o.ID +} + +func (o *GetStatisticsDevice) GetName() *string { + if o == nil { + return nil + } + return o.Name +} + +func (o *GetStatisticsDevice) GetPlatform() *string { + if o == nil { + return nil + } + return o.Platform +} + +func (o *GetStatisticsDevice) GetClientIdentifier() *string { + if o == nil { + return nil + } + return o.ClientIdentifier +} + +func (o *GetStatisticsDevice) GetCreatedAt() *int { + if o == nil { + return nil + } + return o.CreatedAt +} + +type Account struct { + ID *int `json:"id,omitempty"` + Key *string `json:"key,omitempty"` + Name *string `json:"name,omitempty"` + DefaultAudioLanguage *string `json:"defaultAudioLanguage,omitempty"` + AutoSelectAudio *bool `json:"autoSelectAudio,omitempty"` + DefaultSubtitleLanguage *string `json:"defaultSubtitleLanguage,omitempty"` + SubtitleMode *int `json:"subtitleMode,omitempty"` + Thumb *string `json:"thumb,omitempty"` +} + +func (o *Account) GetID() *int { + if o == nil { + return nil + } + return o.ID +} + +func (o *Account) GetKey() *string { + if o == nil { + return nil + } + return o.Key +} + +func (o *Account) GetName() *string { + if o == nil { + return nil + } + return o.Name +} + +func (o *Account) GetDefaultAudioLanguage() *string { + if o == nil { + return nil + } + return o.DefaultAudioLanguage +} + +func (o *Account) GetAutoSelectAudio() *bool { + if o == nil { + return nil + } + return o.AutoSelectAudio +} + +func (o *Account) GetDefaultSubtitleLanguage() *string { + if o == nil { + return nil + } + return o.DefaultSubtitleLanguage +} + +func (o *Account) GetSubtitleMode() *int { + if o == nil { + return nil + } + return o.SubtitleMode +} + +func (o *Account) GetThumb() *string { + if o == nil { + return nil + } + return o.Thumb +} + +type StatisticsMedia struct { + AccountID *int `json:"accountID,omitempty"` + DeviceID *int `json:"deviceID,omitempty"` + Timespan *int `json:"timespan,omitempty"` + At *int `json:"at,omitempty"` + MetadataType *int `json:"metadataType,omitempty"` + Count *int `json:"count,omitempty"` + Duration *int `json:"duration,omitempty"` +} + +func (o *StatisticsMedia) GetAccountID() *int { + if o == nil { + return nil + } + return o.AccountID +} + +func (o *StatisticsMedia) GetDeviceID() *int { + if o == nil { + return nil + } + return o.DeviceID +} + +func (o *StatisticsMedia) GetTimespan() *int { + if o == nil { + return nil + } + return o.Timespan +} + +func (o *StatisticsMedia) GetAt() *int { + if o == nil { + return nil + } + return o.At +} + +func (o *StatisticsMedia) GetMetadataType() *int { + if o == nil { + return nil + } + return o.MetadataType +} + +func (o *StatisticsMedia) GetCount() *int { + if o == nil { + return nil + } + return o.Count +} + +func (o *StatisticsMedia) GetDuration() *int { + if o == nil { + return nil + } + return o.Duration +} + +type GetStatisticsMediaContainer struct { + Size *int `json:"size,omitempty"` + Device []GetStatisticsDevice `json:"Device,omitempty"` + Account []Account `json:"Account,omitempty"` + StatisticsMedia []StatisticsMedia `json:"StatisticsMedia,omitempty"` +} + +func (o *GetStatisticsMediaContainer) GetSize() *int { + if o == nil { + return nil + } + return o.Size +} + +func (o *GetStatisticsMediaContainer) GetDevice() []GetStatisticsDevice { + if o == nil { + return nil + } + return o.Device +} + +func (o *GetStatisticsMediaContainer) GetAccount() []Account { + if o == nil { + return nil + } + return o.Account +} + +func (o *GetStatisticsMediaContainer) GetStatisticsMedia() []StatisticsMedia { + if o == nil { + return nil + } + return o.StatisticsMedia +} + +// GetStatisticsResponseBody - Media Statistics +type GetStatisticsResponseBody struct { + MediaContainer *GetStatisticsMediaContainer `json:"MediaContainer,omitempty"` +} + +func (o *GetStatisticsResponseBody) GetMediaContainer() *GetStatisticsMediaContainer { + if o == nil { + return nil + } + return o.MediaContainer +} + +type GetStatisticsResponse struct { + // HTTP response content type for this operation + ContentType string + // HTTP response status code for this operation + StatusCode int + // Raw HTTP response; suitable for custom response parsing + RawResponse *http.Response + // Media Statistics + Object *GetStatisticsResponseBody +} + +func (o *GetStatisticsResponse) GetContentType() string { + if o == nil { + return "" + } + return o.ContentType +} + +func (o *GetStatisticsResponse) GetStatusCode() int { + if o == nil { + return 0 + } + return o.StatusCode +} + +func (o *GetStatisticsResponse) GetRawResponse() *http.Response { + if o == nil { + return nil + } + return o.RawResponse +} + +func (o *GetStatisticsResponse) GetObject() *GetStatisticsResponseBody { + if o == nil { + return nil + } + return o.Object +} diff --git a/models/sdkerrors/getstatistics.go b/models/sdkerrors/getstatistics.go new file mode 100644 index 0000000..0c7d140 --- /dev/null +++ b/models/sdkerrors/getstatistics.go @@ -0,0 +1,49 @@ +// Code generated by Speakeasy (https://speakeasyapi.dev). DO NOT EDIT. + +package sdkerrors + +import ( + "encoding/json" + "net/http" +) + +type GetStatisticsErrors struct { + Code *float64 `json:"code,omitempty"` + Message *string `json:"message,omitempty"` + Status *float64 `json:"status,omitempty"` +} + +func (o *GetStatisticsErrors) GetCode() *float64 { + if o == nil { + return nil + } + return o.Code +} + +func (o *GetStatisticsErrors) GetMessage() *string { + if o == nil { + return nil + } + return o.Message +} + +func (o *GetStatisticsErrors) GetStatus() *float64 { + if o == nil { + return nil + } + return o.Status +} + +// GetStatisticsResponseBody - Unauthorized - Returned if the X-Plex-Token is missing from the header or query. +type GetStatisticsResponseBody struct { + Errors []GetStatisticsErrors `json:"errors,omitempty"` + // Raw HTTP response; suitable for custom response parsing + RawResponse *http.Response `json:"-"` +} + +var _ error = &GetStatisticsResponseBody{} + +func (e *GetStatisticsResponseBody) Error() string { + data, _ := json.Marshal(e) + return string(data) +} diff --git a/playlists.go b/playlists.go index 99ad2d5..2707093 100644 --- a/playlists.go +++ b/playlists.go @@ -6,12 +6,13 @@ import ( "bytes" "context" "fmt" + "github.com/LukeHagar/plexgo/internal/hooks" "github.com/LukeHagar/plexgo/internal/utils" "github.com/LukeHagar/plexgo/models/operations" "github.com/LukeHagar/plexgo/models/sdkerrors" "io" "net/http" - "strings" + "net/url" ) // Playlists are ordered collections of media. They can be dumb (just a list of media) or smart (based on a media query, such as "all albums from 2017"). @@ -33,30 +34,53 @@ func newPlaylists(sdkConfig sdkConfiguration) *Playlists { // - `uri` - The content URI for what we're playing (e.g. `server://1234/com.plexapp.plugins.library/library/metadata/1`). // - `playQueueID` - To create a playlist from an existing play queue. func (s *Playlists) CreatePlaylist(ctx context.Context, request operations.CreatePlaylistRequest) (*operations.CreatePlaylistResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/playlists" + hookCtx := hooks.HookContext{OperationID: "createPlaylist"} - req, err := http.NewRequestWithContext(ctx, "POST", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/playlists") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.CreatePlaylistResponse{ @@ -71,6 +95,7 @@ func (s *Playlists) CreatePlaylist(ctx context.Context, request operations.Creat } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -111,35 +136,58 @@ func (s *Playlists) CreatePlaylist(ctx context.Context, request operations.Creat // GetPlaylists - Get All Playlists // Get All Playlists given the specified filters. func (s *Playlists) GetPlaylists(ctx context.Context, playlistType *operations.PlaylistType, smart *operations.QueryParamSmart) (*operations.GetPlaylistsResponse, error) { + hookCtx := hooks.HookContext{OperationID: "getPlaylists"} + request := operations.GetPlaylistsRequest{ PlaylistType: playlistType, Smart: smart, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/playlists" + opURL, err := url.JoinPath(baseURL, "/playlists") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetPlaylistsResponse{ @@ -154,6 +202,7 @@ func (s *Playlists) GetPlaylists(ctx context.Context, playlistType *operations.P } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -195,33 +244,53 @@ func (s *Playlists) GetPlaylists(ctx context.Context, playlistType *operations.P // 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. func (s *Playlists) GetPlaylist(ctx context.Context, playlistID float64) (*operations.GetPlaylistResponse, error) { + hookCtx := hooks.HookContext{OperationID: "getPlaylist"} + request := operations.GetPlaylistRequest{ PlaylistID: playlistID, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url, err := utils.GenerateURL(ctx, baseURL, "/playlists/{playlistID}", request, nil) + opURL, err := utils.GenerateURL(ctx, baseURL, "/playlists/{playlistID}", request, nil) if err != nil { return nil, fmt.Errorf("error generating URL: %w", err) } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetPlaylistResponse{ @@ -236,6 +305,7 @@ func (s *Playlists) GetPlaylist(ctx context.Context, playlistID float64) (*opera } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -276,33 +346,53 @@ func (s *Playlists) GetPlaylist(ctx context.Context, playlistID float64) (*opera // DeletePlaylist - Deletes a Playlist // This endpoint will delete a playlist func (s *Playlists) DeletePlaylist(ctx context.Context, playlistID float64) (*operations.DeletePlaylistResponse, error) { + hookCtx := hooks.HookContext{OperationID: "deletePlaylist"} + request := operations.DeletePlaylistRequest{ PlaylistID: playlistID, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url, err := utils.GenerateURL(ctx, baseURL, "/playlists/{playlistID}", request, nil) + opURL, err := utils.GenerateURL(ctx, baseURL, "/playlists/{playlistID}", request, nil) if err != nil { return nil, fmt.Errorf("error generating URL: %w", err) } - req, err := http.NewRequestWithContext(ctx, "DELETE", url, nil) + req, err := http.NewRequestWithContext(ctx, "DELETE", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.DeletePlaylistResponse{ @@ -317,6 +407,7 @@ func (s *Playlists) DeletePlaylist(ctx context.Context, playlistID float64) (*op } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: @@ -346,6 +437,8 @@ func (s *Playlists) DeletePlaylist(ctx context.Context, playlistID float64) (*op // UpdatePlaylist - Update a Playlist // From PMS version 1.9.1 clients can also edit playlist metadata using this endpoint as they would via `PUT /library/metadata/{playlistID}` func (s *Playlists) UpdatePlaylist(ctx context.Context, playlistID float64, title *string, summary *string) (*operations.UpdatePlaylistResponse, error) { + hookCtx := hooks.HookContext{OperationID: "updatePlaylist"} + request := operations.UpdatePlaylistRequest{ PlaylistID: playlistID, Title: title, @@ -353,32 +446,50 @@ func (s *Playlists) UpdatePlaylist(ctx context.Context, playlistID float64, titl } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url, err := utils.GenerateURL(ctx, baseURL, "/playlists/{playlistID}", request, nil) + opURL, err := utils.GenerateURL(ctx, baseURL, "/playlists/{playlistID}", request, nil) if err != nil { return nil, fmt.Errorf("error generating URL: %w", err) } - req, err := http.NewRequestWithContext(ctx, "PUT", url, nil) + req, err := http.NewRequestWithContext(ctx, "PUT", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.UpdatePlaylistResponse{ @@ -393,6 +504,7 @@ func (s *Playlists) UpdatePlaylist(ctx context.Context, playlistID float64, titl } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: @@ -425,38 +537,58 @@ func (s *Playlists) UpdatePlaylist(ctx context.Context, playlistID float64, titl // 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. func (s *Playlists) GetPlaylistContents(ctx context.Context, playlistID float64, type_ float64) (*operations.GetPlaylistContentsResponse, error) { + hookCtx := hooks.HookContext{OperationID: "getPlaylistContents"} + request := operations.GetPlaylistContentsRequest{ PlaylistID: playlistID, Type: type_, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url, err := utils.GenerateURL(ctx, baseURL, "/playlists/{playlistID}/items", request, nil) + opURL, err := utils.GenerateURL(ctx, baseURL, "/playlists/{playlistID}/items", request, nil) if err != nil { return nil, fmt.Errorf("error generating URL: %w", err) } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetPlaylistContentsResponse{ @@ -471,6 +603,7 @@ func (s *Playlists) GetPlaylistContents(ctx context.Context, playlistID float64, } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -511,33 +644,53 @@ func (s *Playlists) GetPlaylistContents(ctx context.Context, playlistID float64, // ClearPlaylistContents - Delete Playlist Contents // Clears a playlist, only works with dumb playlists. Returns the playlist. func (s *Playlists) ClearPlaylistContents(ctx context.Context, playlistID float64) (*operations.ClearPlaylistContentsResponse, error) { + hookCtx := hooks.HookContext{OperationID: "clearPlaylistContents"} + request := operations.ClearPlaylistContentsRequest{ PlaylistID: playlistID, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url, err := utils.GenerateURL(ctx, baseURL, "/playlists/{playlistID}/items", request, nil) + opURL, err := utils.GenerateURL(ctx, baseURL, "/playlists/{playlistID}/items", request, nil) if err != nil { return nil, fmt.Errorf("error generating URL: %w", err) } - req, err := http.NewRequestWithContext(ctx, "DELETE", url, nil) + req, err := http.NewRequestWithContext(ctx, "DELETE", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.ClearPlaylistContentsResponse{ @@ -552,6 +705,7 @@ func (s *Playlists) ClearPlaylistContents(ctx context.Context, playlistID float6 } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: @@ -582,6 +736,8 @@ func (s *Playlists) ClearPlaylistContents(ctx context.Context, playlistID float6 // Adds a generator to a playlist, same parameters as the POST to create. 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. func (s *Playlists) AddPlaylistContents(ctx context.Context, playlistID float64, uri string, playQueueID *float64) (*operations.AddPlaylistContentsResponse, error) { + hookCtx := hooks.HookContext{OperationID: "addPlaylistContents"} + request := operations.AddPlaylistContentsRequest{ PlaylistID: playlistID, URI: uri, @@ -589,32 +745,50 @@ func (s *Playlists) AddPlaylistContents(ctx context.Context, playlistID float64, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url, err := utils.GenerateURL(ctx, baseURL, "/playlists/{playlistID}/items", request, nil) + opURL, err := utils.GenerateURL(ctx, baseURL, "/playlists/{playlistID}/items", request, nil) if err != nil { return nil, fmt.Errorf("error generating URL: %w", err) } - req, err := http.NewRequestWithContext(ctx, "PUT", url, nil) + req, err := http.NewRequestWithContext(ctx, "PUT", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.AddPlaylistContentsResponse{ @@ -629,6 +803,7 @@ func (s *Playlists) AddPlaylistContents(ctx context.Context, playlistID float64, } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -669,35 +844,58 @@ func (s *Playlists) AddPlaylistContents(ctx context.Context, playlistID float64, // UploadPlaylist - Upload Playlist // 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. func (s *Playlists) UploadPlaylist(ctx context.Context, path string, force operations.Force) (*operations.UploadPlaylistResponse, error) { + hookCtx := hooks.HookContext{OperationID: "uploadPlaylist"} + request := operations.UploadPlaylistRequest{ Path: path, Force: force, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/playlists/upload" + opURL, err := url.JoinPath(baseURL, "/playlists/upload") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } - req, err := http.NewRequestWithContext(ctx, "POST", url, nil) + req, err := http.NewRequestWithContext(ctx, "POST", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.UploadPlaylistResponse{ @@ -712,6 +910,7 @@ func (s *Playlists) UploadPlaylist(ctx context.Context, path string, force opera } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: diff --git a/plex.go b/plex.go index 3f3f9a3..36f2a1f 100644 --- a/plex.go +++ b/plex.go @@ -6,12 +6,13 @@ import ( "bytes" "context" "fmt" + "github.com/LukeHagar/plexgo/internal/hooks" "github.com/LukeHagar/plexgo/internal/utils" "github.com/LukeHagar/plexgo/models/operations" "github.com/LukeHagar/plexgo/models/sdkerrors" "io" "net/http" - "strings" + "net/url" ) // Plex - API Calls that perform operations directly against https://Plex.tv @@ -28,6 +29,8 @@ func newPlex(sdkConfig sdkConfiguration) *Plex { // GetPin - Get a Pin // Retrieve a Pin from Plex.tv for authentication flows func (s *Plex) GetPin(ctx context.Context, xPlexClientIdentifier string, strong *bool, opts ...operations.Option) (*operations.GetPinResponse, error) { + hookCtx := hooks.HookContext{OperationID: "getPin"} + request := operations.GetPinRequest{ XPlexClientIdentifier: xPlexClientIdentifier, Strong: strong, @@ -48,14 +51,17 @@ func (s *Plex) GetPin(ctx context.Context, xPlexClientIdentifier string, strong baseURL = *o.ServerURL } - url := strings.TrimSuffix(baseURL, "/") + "/pins" + opURL, err := url.JoinPath(baseURL, "/pins") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } - req, err := http.NewRequestWithContext(ctx, "POST", url, nil) + req, err := http.NewRequestWithContext(ctx, "POST", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) utils.PopulateHeaders(ctx, req, request) @@ -63,16 +69,34 @@ func (s *Plex) GetPin(ctx context.Context, xPlexClientIdentifier string, strong return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetPinResponse{ @@ -87,6 +111,7 @@ func (s *Plex) GetPin(ctx context.Context, xPlexClientIdentifier string, strong } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -125,6 +150,8 @@ func (s *Plex) GetPin(ctx context.Context, xPlexClientIdentifier string, strong // GetToken - Get Access Token // Retrieve an Access Token from Plex.tv after the Pin has already been authenticated func (s *Plex) GetToken(ctx context.Context, pinID string, xPlexClientIdentifier string, opts ...operations.Option) (*operations.GetTokenResponse, error) { + hookCtx := hooks.HookContext{OperationID: "getToken"} + request := operations.GetTokenRequest{ PinID: pinID, XPlexClientIdentifier: xPlexClientIdentifier, @@ -145,30 +172,48 @@ func (s *Plex) GetToken(ctx context.Context, pinID string, xPlexClientIdentifier baseURL = *o.ServerURL } - url, err := utils.GenerateURL(ctx, baseURL, "/pins/{pinID}", request, nil) + opURL, err := utils.GenerateURL(ctx, baseURL, "/pins/{pinID}", request, nil) if err != nil { return nil, fmt.Errorf("error generating URL: %w", err) } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) utils.PopulateHeaders(ctx, req, request) + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetTokenResponse{ @@ -183,6 +228,7 @@ func (s *Plex) GetToken(ctx context.Context, pinID string, xPlexClientIdentifier } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: diff --git a/plexapi.go b/plexapi.go index 53bf840..ee601c9 100644 --- a/plexapi.go +++ b/plexapi.go @@ -6,6 +6,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/LukeHagar/plexgo/internal/hooks" "github.com/LukeHagar/plexgo/internal/utils" "github.com/LukeHagar/plexgo/models/components" "net/http" @@ -54,6 +55,7 @@ type sdkConfiguration struct { GenVersion string UserAgent string RetryConfig *utils.RetryConfig + Hooks *hooks.Hooks } func (c *sdkConfiguration) GetServerDetails() (string, map[string]string) { @@ -73,6 +75,9 @@ type PlexAPI struct { // API Calls interacting with Plex Media Server Media // Media *Media + // API Calls that perform operations with Plex Media Server Videos + // + Video *Video // Activities are awesome. They provide a way to monitor and control asynchronous operations on the server. In order to receive real-time updates for activities, a client would normally subscribe via either EventSource or Websocket endpoints. // Activities are associated with HTTP replies via a special `X-Plex-Activity` header which contains the UUID of the activity. // Activities are optional cancellable. If cancellable, they may be cancelled via the `DELETE` endpoint. Other details: @@ -109,6 +114,9 @@ type PlexAPI struct { // API Calls against Security for Plex Media Server // Security *Security + // API Calls that perform operations with Plex Media Server Statistics + // + Statistics *Statistics // API Calls that perform search operations with Plex Media Server Sessions // Sessions *Sessions @@ -116,9 +124,6 @@ type PlexAPI struct { // Updates to the status can be observed via the Event API. // Updater *Updater - // API Calls that perform operations with Plex Media Server Videos - // - Video *Video sdkConfiguration sdkConfiguration } @@ -263,9 +268,9 @@ func New(opts ...SDKOption) *PlexAPI { sdkConfiguration: sdkConfiguration{ Language: "go", OpenAPIDocVersion: "0.0.3", - SDKVersion: "0.3.0", - GenVersion: "2.249.1", - UserAgent: "speakeasy-sdk/go 0.3.0 2.249.1 0.0.3 github.com/LukeHagar/plexgo", + SDKVersion: "0.4.0", + GenVersion: "2.269.0", + UserAgent: "speakeasy-sdk/go 0.4.0 2.269.0 0.0.3 github.com/LukeHagar/plexgo", ServerDefaults: []map[string]string{ { "protocol": "http", @@ -273,12 +278,15 @@ func New(opts ...SDKOption) *PlexAPI { "port": "32400", }, }, + Hooks: hooks.New(), }, } for _, opt := range opts { opt(sdk) } + sdk.sdkConfiguration.DefaultClient = sdk.sdkConfiguration.Hooks.ClientInit(sdk.sdkConfiguration.DefaultClient) + // Use WithClient to override the default client if you would like to customize the timeout if sdk.sdkConfiguration.DefaultClient == nil { sdk.sdkConfiguration.DefaultClient = &http.Client{Timeout: 60 * time.Second} @@ -295,6 +303,8 @@ func New(opts ...SDKOption) *PlexAPI { sdk.Media = newMedia(sdk.sdkConfiguration) + sdk.Video = newVideo(sdk.sdkConfiguration) + sdk.Activities = newActivities(sdk.sdkConfiguration) sdk.Butler = newButler(sdk.sdkConfiguration) @@ -313,11 +323,11 @@ func New(opts ...SDKOption) *PlexAPI { sdk.Security = newSecurity(sdk.sdkConfiguration) + sdk.Statistics = newStatistics(sdk.sdkConfiguration) + sdk.Sessions = newSessions(sdk.sdkConfiguration) sdk.Updater = newUpdater(sdk.sdkConfiguration) - sdk.Video = newVideo(sdk.sdkConfiguration) - return sdk } diff --git a/search.go b/search.go index d2f0e6e..133b068 100644 --- a/search.go +++ b/search.go @@ -6,12 +6,13 @@ import ( "bytes" "context" "fmt" + "github.com/LukeHagar/plexgo/internal/hooks" "github.com/LukeHagar/plexgo/internal/utils" "github.com/LukeHagar/plexgo/models/operations" "github.com/LukeHagar/plexgo/models/sdkerrors" "io" "net/http" - "strings" + "net/url" ) // Search - API Calls that perform search operations with Plex Media Server @@ -40,6 +41,8 @@ func newSearch(sdkConfig sdkConfiguration) *Search { // // This request is intended to be very fast, and called as the user types. func (s *Search) PerformSearch(ctx context.Context, query string, sectionID *float64, limit *float64) (*operations.PerformSearchResponse, error) { + hookCtx := hooks.HookContext{OperationID: "performSearch"} + request := operations.PerformSearchRequest{ Query: query, SectionID: sectionID, @@ -47,29 +50,50 @@ func (s *Search) PerformSearch(ctx context.Context, query string, sectionID *flo } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/hubs/search" + opURL, err := url.JoinPath(baseURL, "/hubs/search") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.PerformSearchResponse{ @@ -84,6 +108,7 @@ func (s *Search) PerformSearch(ctx context.Context, query string, sectionID *flo } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: @@ -116,6 +141,8 @@ func (s *Search) PerformSearch(ctx context.Context, query string, sectionID *flo // 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. func (s *Search) PerformVoiceSearch(ctx context.Context, query string, sectionID *float64, limit *float64) (*operations.PerformVoiceSearchResponse, error) { + hookCtx := hooks.HookContext{OperationID: "performVoiceSearch"} + request := operations.PerformVoiceSearchRequest{ Query: query, SectionID: sectionID, @@ -123,29 +150,50 @@ func (s *Search) PerformVoiceSearch(ctx context.Context, query string, sectionID } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/hubs/search/voice" + opURL, err := url.JoinPath(baseURL, "/hubs/search/voice") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.PerformVoiceSearchResponse{ @@ -160,6 +208,7 @@ func (s *Search) PerformVoiceSearch(ctx context.Context, query string, sectionID } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: @@ -189,34 +238,57 @@ func (s *Search) PerformVoiceSearch(ctx context.Context, query string, sectionID // GetSearchResults - Get Search Results // This will search the database for the string provided. func (s *Search) GetSearchResults(ctx context.Context, query string) (*operations.GetSearchResultsResponse, error) { + hookCtx := hooks.HookContext{OperationID: "getSearchResults"} + request := operations.GetSearchResultsRequest{ Query: query, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/search" + opURL, err := url.JoinPath(baseURL, "/search") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetSearchResultsResponse{ @@ -231,6 +303,7 @@ func (s *Search) GetSearchResults(ctx context.Context, query string) (*operation } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { diff --git a/security.go b/security.go index e08e07d..361caa6 100644 --- a/security.go +++ b/security.go @@ -6,12 +6,13 @@ import ( "bytes" "context" "fmt" + "github.com/LukeHagar/plexgo/internal/hooks" "github.com/LukeHagar/plexgo/internal/utils" "github.com/LukeHagar/plexgo/models/operations" "github.com/LukeHagar/plexgo/models/sdkerrors" "io" "net/http" - "strings" + "net/url" ) // Security - API Calls against Security for Plex Media Server @@ -28,35 +29,58 @@ func newSecurity(sdkConfig sdkConfiguration) *Security { // GetTransientToken - Get a Transient Token. // 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. func (s *Security) GetTransientToken(ctx context.Context, type_ operations.GetTransientTokenQueryParamType, scope operations.Scope) (*operations.GetTransientTokenResponse, error) { + hookCtx := hooks.HookContext{OperationID: "getTransientToken"} + request := operations.GetTransientTokenRequest{ Type: type_, Scope: scope, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/security/token" + opURL, err := url.JoinPath(baseURL, "/security/token") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetTransientTokenResponse{ @@ -71,6 +95,7 @@ func (s *Security) GetTransientToken(ctx context.Context, type_ operations.GetTr } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: @@ -101,34 +126,57 @@ func (s *Security) GetTransientToken(ctx context.Context, type_ operations.GetTr // 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. func (s *Security) GetSourceConnectionInformation(ctx context.Context, source string) (*operations.GetSourceConnectionInformationResponse, error) { + hookCtx := hooks.HookContext{OperationID: "getSourceConnectionInformation"} + request := operations.GetSourceConnectionInformationRequest{ Source: source, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/security/resources" + opURL, err := url.JoinPath(baseURL, "/security/resources") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetSourceConnectionInformationResponse{ @@ -143,6 +191,7 @@ func (s *Security) GetSourceConnectionInformation(ctx context.Context, source st } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: diff --git a/server.go b/server.go index 2ed9cab..2644901 100644 --- a/server.go +++ b/server.go @@ -6,12 +6,13 @@ import ( "bytes" "context" "fmt" + "github.com/LukeHagar/plexgo/internal/hooks" "github.com/LukeHagar/plexgo/internal/utils" "github.com/LukeHagar/plexgo/models/operations" "github.com/LukeHagar/plexgo/models/sdkerrors" "io" "net/http" - "strings" + "net/url" ) // Server - Operations against the Plex Media Server System. @@ -28,26 +29,49 @@ func newServer(sdkConfig sdkConfiguration) *Server { // GetServerCapabilities - Server Capabilities // Server Capabilities func (s *Server) GetServerCapabilities(ctx context.Context) (*operations.GetServerCapabilitiesResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/" + hookCtx := hooks.HookContext{OperationID: "getServerCapabilities"} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetServerCapabilitiesResponse{ @@ -62,6 +86,7 @@ func (s *Server) GetServerCapabilities(ctx context.Context) (*operations.GetServ } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -102,26 +127,49 @@ func (s *Server) GetServerCapabilities(ctx context.Context) (*operations.GetServ // GetServerPreferences - Get Server Preferences // Get Server Preferences func (s *Server) GetServerPreferences(ctx context.Context) (*operations.GetServerPreferencesResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/:/prefs" + hookCtx := hooks.HookContext{OperationID: "getServerPreferences"} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/:/prefs") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetServerPreferencesResponse{ @@ -136,6 +184,7 @@ func (s *Server) GetServerPreferences(ctx context.Context) (*operations.GetServe } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -176,26 +225,49 @@ func (s *Server) GetServerPreferences(ctx context.Context) (*operations.GetServe // GetAvailableClients - Get Available Clients // Get Available Clients func (s *Server) GetAvailableClients(ctx context.Context) (*operations.GetAvailableClientsResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/clients" + hookCtx := hooks.HookContext{OperationID: "getAvailableClients"} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/clients") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetAvailableClientsResponse{ @@ -210,6 +282,7 @@ func (s *Server) GetAvailableClients(ctx context.Context) (*operations.GetAvaila } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -250,26 +323,49 @@ func (s *Server) GetAvailableClients(ctx context.Context) (*operations.GetAvaila // GetDevices - Get Devices // Get Devices func (s *Server) GetDevices(ctx context.Context) (*operations.GetDevicesResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/devices" + hookCtx := hooks.HookContext{OperationID: "getDevices"} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/devices") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetDevicesResponse{ @@ -284,6 +380,7 @@ func (s *Server) GetDevices(ctx context.Context) (*operations.GetDevicesResponse } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -324,26 +421,49 @@ func (s *Server) GetDevices(ctx context.Context) (*operations.GetDevicesResponse // GetServerIdentity - Get Server Identity // Get Server Identity func (s *Server) GetServerIdentity(ctx context.Context) (*operations.GetServerIdentityResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/identity" + hookCtx := hooks.HookContext{OperationID: "getServerIdentity"} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/identity") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetServerIdentityResponse{ @@ -358,6 +478,7 @@ func (s *Server) GetServerIdentity(ctx context.Context) (*operations.GetServerId } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -398,26 +519,49 @@ func (s *Server) GetServerIdentity(ctx context.Context) (*operations.GetServerId // GetMyPlexAccount - Get MyPlex Account // Returns MyPlex Account Information func (s *Server) GetMyPlexAccount(ctx context.Context) (*operations.GetMyPlexAccountResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/myplex/account" + hookCtx := hooks.HookContext{OperationID: "getMyPlexAccount"} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/myplex/account") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetMyPlexAccountResponse{ @@ -432,6 +576,7 @@ func (s *Server) GetMyPlexAccount(ctx context.Context) (*operations.GetMyPlexAcc } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -472,30 +617,53 @@ func (s *Server) GetMyPlexAccount(ctx context.Context) (*operations.GetMyPlexAcc // GetResizedPhoto - Get a Resized Photo // Plex's Photo transcoder is used throughout the service to serve images at specified sizes. func (s *Server) GetResizedPhoto(ctx context.Context, request operations.GetResizedPhotoRequest) (*operations.GetResizedPhotoResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/photo/:/transcode" + hookCtx := hooks.HookContext{OperationID: "getResizedPhoto"} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/photo/:/transcode") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetResizedPhotoResponse{ @@ -510,6 +678,7 @@ func (s *Server) GetResizedPhoto(ctx context.Context, request operations.GetResi } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: @@ -539,26 +708,49 @@ func (s *Server) GetResizedPhoto(ctx context.Context, request operations.GetResi // GetServerList - Get Server List // Get Server List func (s *Server) GetServerList(ctx context.Context) (*operations.GetServerListResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/servers" + hookCtx := hooks.HookContext{OperationID: "getServerList"} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/servers") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetServerListResponse{ @@ -573,6 +765,7 @@ func (s *Server) GetServerList(ctx context.Context) (*operations.GetServerListRe } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { diff --git a/sessions.go b/sessions.go index d287d6a..1e04e1a 100644 --- a/sessions.go +++ b/sessions.go @@ -6,12 +6,13 @@ import ( "bytes" "context" "fmt" + "github.com/LukeHagar/plexgo/internal/hooks" "github.com/LukeHagar/plexgo/internal/utils" "github.com/LukeHagar/plexgo/models/operations" "github.com/LukeHagar/plexgo/models/sdkerrors" "io" "net/http" - "strings" + "net/url" ) // Sessions - API Calls that perform search operations with Plex Media Server Sessions @@ -28,26 +29,49 @@ func newSessions(sdkConfig sdkConfiguration) *Sessions { // GetSessions - Get Active Sessions // This will retrieve the "Now Playing" Information of the PMS. func (s *Sessions) GetSessions(ctx context.Context) (*operations.GetSessionsResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/status/sessions" + hookCtx := hooks.HookContext{OperationID: "getSessions"} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/status/sessions") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetSessionsResponse{ @@ -62,6 +86,7 @@ func (s *Sessions) GetSessions(ctx context.Context) (*operations.GetSessionsResp } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -102,26 +127,49 @@ func (s *Sessions) GetSessions(ctx context.Context) (*operations.GetSessionsResp // GetSessionHistory - Get Session History // This will Retrieve a listing of all history views. func (s *Sessions) GetSessionHistory(ctx context.Context) (*operations.GetSessionHistoryResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/status/sessions/history/all" + hookCtx := hooks.HookContext{OperationID: "getSessionHistory"} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/status/sessions/history/all") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetSessionHistoryResponse{ @@ -136,6 +184,7 @@ func (s *Sessions) GetSessionHistory(ctx context.Context) (*operations.GetSessio } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -176,26 +225,49 @@ func (s *Sessions) GetSessionHistory(ctx context.Context) (*operations.GetSessio // GetTranscodeSessions - Get Transcode Sessions // Get Transcode Sessions func (s *Sessions) GetTranscodeSessions(ctx context.Context) (*operations.GetTranscodeSessionsResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/transcode/sessions" + hookCtx := hooks.HookContext{OperationID: "getTranscodeSessions"} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/transcode/sessions") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetTranscodeSessionsResponse{ @@ -210,6 +282,7 @@ func (s *Sessions) GetTranscodeSessions(ctx context.Context) (*operations.GetTra } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -250,33 +323,53 @@ func (s *Sessions) GetTranscodeSessions(ctx context.Context) (*operations.GetTra // StopTranscodeSession - Stop a Transcode Session // Stop a Transcode Session func (s *Sessions) StopTranscodeSession(ctx context.Context, sessionKey string) (*operations.StopTranscodeSessionResponse, error) { + hookCtx := hooks.HookContext{OperationID: "stopTranscodeSession"} + request := operations.StopTranscodeSessionRequest{ SessionKey: sessionKey, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url, err := utils.GenerateURL(ctx, baseURL, "/transcode/sessions/{sessionKey}", request, nil) + opURL, err := utils.GenerateURL(ctx, baseURL, "/transcode/sessions/{sessionKey}", request, nil) if err != nil { return nil, fmt.Errorf("error generating URL: %w", err) } - req, err := http.NewRequestWithContext(ctx, "DELETE", url, nil) + req, err := http.NewRequestWithContext(ctx, "DELETE", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.StopTranscodeSessionResponse{ @@ -291,6 +384,7 @@ func (s *Sessions) StopTranscodeSession(ctx context.Context, sessionKey string) } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 204: case httpRes.StatusCode == 400: diff --git a/statistics.go b/statistics.go new file mode 100644 index 0000000..bac3678 --- /dev/null +++ b/statistics.go @@ -0,0 +1,133 @@ +// Code generated by Speakeasy (https://speakeasyapi.dev). DO NOT EDIT. + +package plexgo + +import ( + "bytes" + "context" + "fmt" + "github.com/LukeHagar/plexgo/internal/hooks" + "github.com/LukeHagar/plexgo/internal/utils" + "github.com/LukeHagar/plexgo/models/operations" + "github.com/LukeHagar/plexgo/models/sdkerrors" + "io" + "net/http" + "net/url" +) + +// Statistics - API Calls that perform operations with Plex Media Server Statistics +type Statistics struct { + sdkConfiguration sdkConfiguration +} + +func newStatistics(sdkConfig sdkConfiguration) *Statistics { + return &Statistics{ + sdkConfiguration: sdkConfig, + } +} + +// GetStatistics - Get Media Statistics +// This will return the media statistics for the server +func (s *Statistics) GetStatistics(ctx context.Context, timespan *int64) (*operations.GetStatisticsResponse, error) { + hookCtx := hooks.HookContext{OperationID: "getStatistics"} + + request := operations.GetStatisticsRequest{ + Timespan: timespan, + } + + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/statistics/media") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { + return nil, fmt.Errorf("error populating query params: %w", err) + } + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + + client := s.sdkConfiguration.SecurityClient + + httpRes, err := client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + contentType := httpRes.Header.Get("Content-Type") + + res := &operations.GetStatisticsResponse{ + StatusCode: httpRes.StatusCode, + ContentType: contentType, + RawResponse: httpRes, + } + + rawBody, err := io.ReadAll(httpRes.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + httpRes.Body.Close() + httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(contentType, `application/json`): + var out operations.GetStatisticsResponseBody + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Object = &out + default: + return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", contentType), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode == 400: + fallthrough + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + fallthrough + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + return nil, sdkerrors.NewSDKError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode == 401: + switch { + case utils.MatchContentType(contentType, `application/json`): + var out sdkerrors.GetStatisticsResponseBody + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + out.RawResponse = httpRes + + return nil, &out + default: + return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", contentType), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil +} diff --git a/updater.go b/updater.go index 998ccc4..eb1ee13 100644 --- a/updater.go +++ b/updater.go @@ -6,12 +6,13 @@ import ( "bytes" "context" "fmt" + "github.com/LukeHagar/plexgo/internal/hooks" "github.com/LukeHagar/plexgo/internal/utils" "github.com/LukeHagar/plexgo/models/operations" "github.com/LukeHagar/plexgo/models/sdkerrors" "io" "net/http" - "strings" + "net/url" ) // Updater - This describes the API for searching and applying updates to the Plex Media Server. @@ -29,26 +30,49 @@ func newUpdater(sdkConfig sdkConfiguration) *Updater { // GetUpdateStatus - Querying status of updates // Querying status of updates func (s *Updater) GetUpdateStatus(ctx context.Context) (*operations.GetUpdateStatusResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/updater/status" + hookCtx := hooks.HookContext{OperationID: "getUpdateStatus"} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/updater/status") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetUpdateStatusResponse{ @@ -63,6 +87,7 @@ func (s *Updater) GetUpdateStatus(ctx context.Context) (*operations.GetUpdateSta } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: switch { @@ -103,34 +128,57 @@ func (s *Updater) GetUpdateStatus(ctx context.Context) (*operations.GetUpdateSta // CheckForUpdates - Checking for updates // Checking for updates func (s *Updater) CheckForUpdates(ctx context.Context, download *operations.Download) (*operations.CheckForUpdatesResponse, error) { + hookCtx := hooks.HookContext{OperationID: "checkForUpdates"} + request := operations.CheckForUpdatesRequest{ Download: download, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/updater/check" + opURL, err := url.JoinPath(baseURL, "/updater/check") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } - req, err := http.NewRequestWithContext(ctx, "PUT", url, nil) + req, err := http.NewRequestWithContext(ctx, "PUT", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.CheckForUpdatesResponse{ @@ -145,6 +193,7 @@ func (s *Updater) CheckForUpdates(ctx context.Context, download *operations.Down } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: @@ -174,35 +223,58 @@ func (s *Updater) CheckForUpdates(ctx context.Context, download *operations.Down // ApplyUpdates - Apply Updates // Note that these two parameters are effectively mutually exclusive. The `tonight` parameter takes precedence and `skip` will be ignored if `tonight` is also passed func (s *Updater) ApplyUpdates(ctx context.Context, tonight *operations.Tonight, skip *operations.Skip) (*operations.ApplyUpdatesResponse, error) { + hookCtx := hooks.HookContext{OperationID: "applyUpdates"} + request := operations.ApplyUpdatesRequest{ Tonight: tonight, Skip: skip, } baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/updater/apply" + opURL, err := url.JoinPath(baseURL, "/updater/apply") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } - req, err := http.NewRequestWithContext(ctx, "PUT", url, nil) + req, err := http.NewRequestWithContext(ctx, "PUT", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "500", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.ApplyUpdatesResponse{ @@ -217,6 +289,7 @@ func (s *Updater) ApplyUpdates(ctx context.Context, tonight *operations.Tonight, } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: diff --git a/video.go b/video.go index 907af45..007cc20 100644 --- a/video.go +++ b/video.go @@ -6,12 +6,13 @@ import ( "bytes" "context" "fmt" + "github.com/LukeHagar/plexgo/internal/hooks" "github.com/LukeHagar/plexgo/internal/utils" "github.com/LukeHagar/plexgo/models/operations" "github.com/LukeHagar/plexgo/models/sdkerrors" "io" "net/http" - "strings" + "net/url" ) // Video - API Calls that perform operations with Plex Media Server Videos @@ -25,100 +26,56 @@ func newVideo(sdkConfig sdkConfiguration) *Video { } } -// StartUniversalTranscode - Start Universal Transcode -// Begin a Universal Transcode Session -func (s *Video) StartUniversalTranscode(ctx context.Context, request operations.StartUniversalTranscodeRequest) (*operations.StartUniversalTranscodeResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/video/:/transcode/universal/start.mpd" - - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - return nil, fmt.Errorf("error creating request: %w", err) - } - req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) - - if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { - return nil, fmt.Errorf("error populating query params: %w", err) - } - - client := s.sdkConfiguration.SecurityClient - - httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } - - contentType := httpRes.Header.Get("Content-Type") - - res := &operations.StartUniversalTranscodeResponse{ - StatusCode: httpRes.StatusCode, - ContentType: contentType, - RawResponse: httpRes, - } - - rawBody, err := io.ReadAll(httpRes.Body) - if err != nil { - return nil, fmt.Errorf("error reading response body: %w", err) - } - httpRes.Body.Close() - httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) - switch { - case httpRes.StatusCode == 200: - case httpRes.StatusCode == 400: - fallthrough - case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: - fallthrough - case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: - return nil, sdkerrors.NewSDKError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) - case httpRes.StatusCode == 401: - switch { - case utils.MatchContentType(contentType, `application/json`): - var out sdkerrors.StartUniversalTranscodeResponseBody - if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { - return nil, err - } - out.RawResponse = httpRes - - return nil, &out - default: - return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", contentType), httpRes.StatusCode, string(rawBody), httpRes) - } - } - - return res, nil -} - // GetTimeline - Get the timeline for a media item // Get the timeline for a media item func (s *Video) GetTimeline(ctx context.Context, request operations.GetTimelineRequest) (*operations.GetTimelineResponse, error) { - baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) - url := strings.TrimSuffix(baseURL, "/") + "/:/timeline" + hookCtx := hooks.HookContext{OperationID: "getTimeline"} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/:/timeline") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("user-agent", s.sdkConfiguration.UserAgent) + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { return nil, fmt.Errorf("error populating query params: %w", err) } + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + client := s.sdkConfiguration.SecurityClient httpRes, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - if httpRes == nil { - return nil, fmt.Errorf("error sending request: no response") - } + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } contentType := httpRes.Header.Get("Content-Type") res := &operations.GetTimelineResponse{ @@ -133,6 +90,7 @@ func (s *Video) GetTimeline(ctx context.Context, request operations.GetTimelineR } httpRes.Body.Close() httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + switch { case httpRes.StatusCode == 200: case httpRes.StatusCode == 400: @@ -158,3 +116,94 @@ func (s *Video) GetTimeline(ctx context.Context, request operations.GetTimelineR return res, nil } + +// StartUniversalTranscode - Start Universal Transcode +// Begin a Universal Transcode Session +func (s *Video) StartUniversalTranscode(ctx context.Context, request operations.StartUniversalTranscodeRequest) (*operations.StartUniversalTranscodeResponse, error) { + hookCtx := hooks.HookContext{OperationID: "startUniversalTranscode"} + + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/video/:/transcode/universal/start.mpd") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { + return nil, fmt.Errorf("error populating query params: %w", err) + } + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{hookCtx}, req) + if err != nil { + return nil, err + } + + client := s.sdkConfiguration.SecurityClient + + httpRes, err := client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"400", "401", "4XX", "5XX"}, httpRes.StatusCode) { + httpRes, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + contentType := httpRes.Header.Get("Content-Type") + + res := &operations.StartUniversalTranscodeResponse{ + StatusCode: httpRes.StatusCode, + ContentType: contentType, + RawResponse: httpRes, + } + + rawBody, err := io.ReadAll(httpRes.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + httpRes.Body.Close() + httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + + switch { + case httpRes.StatusCode == 200: + case httpRes.StatusCode == 400: + fallthrough + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + fallthrough + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + return nil, sdkerrors.NewSDKError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode == 401: + switch { + case utils.MatchContentType(contentType, `application/json`): + var out sdkerrors.StartUniversalTranscodeResponseBody + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + out.RawResponse = httpRes + + return nil, &out + default: + return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", contentType), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil +}