├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── analytics.go ├── analytics_test.go ├── api.go ├── api_test.go ├── channels.go ├── channels_test.go ├── drm_policies.go ├── drm_policies_test.go ├── events.go ├── events_test.go ├── go.mod ├── go.sum ├── imports.go ├── imports_test.go ├── jwplatform.go ├── media.go ├── media_test.go ├── player_bidding.go ├── player_bidding_test.go ├── v1 ├── README.md ├── go.mod ├── go.sum ├── upload.go ├── upload_test.go ├── v1.go └── v1_test.go ├── webhooks.go └── webhooks_test.go /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please only file issues here that you believe represent actual bugs or feature requests for the JW Platform Go library. 2 | 3 | If you are having general trouble with your JW Player integration, please reach out to support using the form at https://support.jwplayer.com (preferred) or via email to support@jwplayer.com. 4 | 5 | If you are reporting a bug, please include your Go version and the version of the JW Platform Go library you're using, as well as any other details that may be helpful in reproducing the problem. 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | # Install various build dependencies. We use `travis_retry` because `go get` 3 | # will occasionally fail intermittently. 4 | 5 | # The testify require framework is used for assertions in the test suite 6 | - travis_retry go get -u github.com/stretchr/testify/require 7 | - travis_retry go get -u github.com/Flaque/filet 8 | - travis_retry go get -u gopkg.in/h2non/gock.v1 9 | 10 | go: 11 | - "1.13.x" 12 | - tip 13 | 14 | language: go 15 | 16 | matrix: 17 | allow_failures: 18 | - go: tip 19 | fast_finish: true 20 | 21 | script: 22 | - make test 23 | 24 | sudo: false 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 1.0.0 (boyntoni) 4 | ---------------- 5 | 6 | * Add client for JW Player V2 APIs 7 | * Former client now accessible under the `v1` namespace 8 | 9 | 0.3.0 (ksindi) 10 | -------------- 11 | 12 | * Fix go mod conflict 13 | 14 | 0.2.0 (ksindi) 15 | -------------- 16 | 17 | * Add Client Upload method 18 | * MakeRequest should only return error 19 | * Fix encoding for signature base string 20 | 21 | 0.1.0 (ksindi) 22 | -------------- 23 | 24 | * Initial release 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APPNAME = jwplatform-go 2 | VERSION := $(shell head CHANGELOG.md | grep -e '^[0-9]' | head -n 1 | cut -f 1 -d ' ') 3 | GOPATH ?= $(curdir)/.gopath 4 | 5 | export GOPATH 6 | 7 | all: test 8 | 9 | test: 10 | @echo -e '\e[01;34mRunning Go unit tests\e[0m' 11 | @go test -cover 12 | 13 | release: test 14 | go mod tidy 15 | @echo "Releasing $(APPNAME) v$(VERSION)" 16 | git tag v$(VERSION) 17 | git push --tags 18 | 19 | clean: 20 | @rm -rf build 21 | 22 | distclean: clean 23 | @rm -rf Gopkg.lock 24 | 25 | .PHONY: clean distclean 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go JW Platform 2 | 3 | [![GoDoc](http://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/jwplayer/jwplatform-go) 4 | [![Build Status](https://travis-ci.org/jwplayer/jwplatform-go.svg?branch=master)](https://travis-ci.org/jwplayer/jwplatform-go) 5 | 6 | The official Go client library for accessing the [JW Platform](https://www.jwplayer.com/video-delivery/) API. 7 | 8 | ## Requirements 9 | 10 | Go 1.15+ 11 | 12 | ## Usage 13 | 14 | ```go 15 | import ( 16 | "github.com/jwplayer/jwplatform-go" 17 | "github.com/jwplayer/jwplatform-go/media" 18 | ) 19 | 20 | jwplatform := jwplatform.New("API_SECRET") 21 | siteID := "9kzNUpe4" 22 | mediaID := "LaJFzc9d" 23 | 24 | // Get a Resource 25 | media, err := jwplatform.Media.Get(siteID, mediaID) 26 | 27 | // Create a Resource 28 | mediaToCreate := &jwplatform.MediaMetadata(Title: "My new video") 29 | media, err := jwplatform.Media.Create(siteID, mediaToCreate) 30 | 31 | // List a Resource 32 | mediaResources, err := jwplatform.Media.List(siteID, nil) 33 | // Optionally include query parameters, including page, page length, sort, and filters. 34 | params := jwplatform.QueryParams{Page: 2, PageLength: 5} 35 | mediaResources, err := jwplatform.Media.List(siteID, params) 36 | 37 | // Update a Resource 38 | updateMetadata := &jwplatform.MediaMetadata{Title: "Updated video title"} 39 | updatedMedia, err := jwplatform.Media.Update(siteID, mediaID, updateMetadata) 40 | 41 | // Delete a Resource 42 | _ := jwplatform.Media.Delete(siteID, mediaID) 43 | ``` 44 | 45 | ## Supported operations 46 | 47 | All API methods documentated on the API are available in this client. Please refer to our [api documentation](https://developer.jwplayer.com/jwplayer/reference#introduction-to-api-v2). 48 | 49 | ## Test 50 | 51 | Before running the tests, make sure to grab all of the package's dependencies: 52 | 53 | go get -t -v 54 | 55 | Run all tests: 56 | 57 | make test 58 | 59 | For any requests, bug or comments, please [open an issue][issues] or [submit a 60 | pull request][pulls]. 61 | 62 | ## V1 Client 63 | 64 | The V1 Client remains available for use, but is deprecated. We strongly recommend using the V2 Client. For documentation on the V1 Client, please refer to the `v1` submodule. 65 | -------------------------------------------------------------------------------- /analytics.go: -------------------------------------------------------------------------------- 1 | package jwplatform 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/google/go-querystring/query" 8 | ) 9 | 10 | // AnalyticsQueryParameters define the allowed parameters on the Query action. 11 | type AnalyticsQueryParameters struct { 12 | Source string `url:"source"` 13 | Format string `url:"format"` 14 | } 15 | 16 | // AnalyticsResponse is the structure returned via the Query action. 17 | type AnalyticsResponse struct { 18 | Dimensions []string `json:"dimensions"` 19 | StartDate string `json:"start_date"` 20 | EndDate string `json:"end_date"` 21 | Filter string `json:"filter"` 22 | IncludeMetadata bool `json:"include_metadata"` 23 | Metrics []reportMetric `json:"metrics"` 24 | Sort []reportSort `json:"sort"` 25 | Page int `json:"page"` 26 | PageLength int `json:"page_length"` 27 | RelativeTimeframe string `json:"relative_timeframe"` 28 | } 29 | 30 | type reportMetric struct { 31 | Field string `json:"field"` 32 | Operation string `json:"operation"` 33 | } 34 | 35 | type reportSort struct { 36 | Field string `json:"field"` 37 | Operation string `json:"operation"` 38 | Order string `json:"order"` 39 | } 40 | 41 | // AnalyticsClient for interacting with V2 Analytics API. 42 | type AnalyticsClient struct { 43 | v2Client *V2Client 44 | } 45 | 46 | // Query the Analytics API 47 | func (c *AnalyticsClient) Query(siteID string, queryParams *AnalyticsQueryParameters) (*AnalyticsResponse, error) { 48 | analyticResponse := &AnalyticsResponse{} 49 | path := fmt.Sprintf("/v2/sites/%s/analytics/queries", siteID) 50 | urlValues, _ := query.Values(queryParams) 51 | err := c.v2Client.Request(http.MethodPost, path, analyticResponse, nil, urlValues) 52 | return analyticResponse, err 53 | } 54 | -------------------------------------------------------------------------------- /analytics_test.go: -------------------------------------------------------------------------------- 1 | package jwplatform 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "gopkg.in/h2non/gock.v1" 10 | ) 11 | 12 | func TestAnalyticsQuery(t *testing.T) { 13 | defer gock.Off() 14 | 15 | siteID := "abcdefgh" 16 | mockAuthToken := "shhh" 17 | 18 | requestPath := fmt.Sprintf("/v2/sites/%s/analytics/queries", siteID) 19 | mockResponse := map[string]bool{"include_metadata": true} 20 | 21 | gock.New("https://api.jwplayer.com"). 22 | Post(requestPath). 23 | MatchHeader("Authorization", "^Bearer .+"). 24 | MatchHeader("User-Agent", "^jwplatform-go/+"). 25 | MatchParam("source", "default"). 26 | MatchParam("format", "json"). 27 | Reply(200). 28 | JSON(mockResponse) 29 | 30 | testClient := New(mockAuthToken) 31 | analyticsParams := &AnalyticsQueryParameters{Source: "default", Format: "json"} 32 | analyticsResp, _ := testClient.Analytics.Query(siteID, analyticsParams) 33 | assert.Equal(t, true, analyticsResp.IncludeMetadata) 34 | } 35 | 36 | func TestUnmarshalAnalyticsResponse(t *testing.T) { 37 | analyticsData := map[string]interface{}{ 38 | "dimensions": []string{"dimension_a", "dimension_b"}, 39 | "start_date": "2019-09-25T15:29:11.042095+00:00", 40 | "end_date": "2019-09-25T15:29:11.042095+00:00", 41 | "filter": "a_filter", 42 | "include_metadata": true, 43 | "metrics": []interface{}{ 44 | map[string]string{ 45 | "field": "field_a", 46 | "operation": "=", 47 | }, 48 | }, 49 | "sort": []interface{}{ 50 | map[string]string{ 51 | "field": "field_b", 52 | "operation": "max", 53 | "order": "ascending", 54 | }, 55 | }, 56 | "page": 1, 57 | "page_length": 10, 58 | "relative_timeframe": "7 Days", 59 | } 60 | 61 | bytes, err := json.Marshal(&analyticsData) 62 | assert.NoError(t, err) 63 | 64 | var analytics AnalyticsResponse 65 | err = json.Unmarshal(bytes, &analytics) 66 | assert.NoError(t, err) 67 | 68 | assert.Equal(t, []string{"dimension_a", "dimension_b"}, analytics.Dimensions) 69 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", analytics.StartDate) 70 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", analytics.EndDate) 71 | assert.Equal(t, "a_filter", analytics.Filter) 72 | assert.Equal(t, 1, analytics.Page) 73 | assert.Equal(t, 10, analytics.PageLength) 74 | assert.Equal(t, "7 Days", analytics.RelativeTimeframe) 75 | assert.Equal(t, true, analytics.IncludeMetadata) 76 | 77 | assert.Equal(t, "field_a", analytics.Metrics[0].Field) 78 | assert.Equal(t, "=", analytics.Metrics[0].Operation) 79 | 80 | assert.Equal(t, "field_b", analytics.Sort[0].Field) 81 | assert.Equal(t, "max", analytics.Sort[0].Operation) 82 | assert.Equal(t, "ascending", analytics.Sort[0].Order) 83 | 84 | } 85 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package jwplatform 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | ) 11 | 12 | // V2Client is a light wrapper around the http.defaultClient for interacting with JW Player V2 Platform APIs 13 | type V2Client struct { 14 | Version string 15 | authToken string 16 | baseURL *url.URL 17 | client *http.Client 18 | } 19 | 20 | // V2ResourcesResponse describes the response structure for list calls 21 | type V2ResourcesResponse struct { 22 | Total int `json:"total"` 23 | Page int `json:"page"` 24 | PageLength int `json:"page_length"` 25 | } 26 | 27 | // V2ResourceResponse describes the response structure for resource calls 28 | type V2ResourceResponse struct { 29 | ID string `json:"id"` 30 | Created string `json:"created"` 31 | LastModified string `json:"last_modified"` 32 | Type string `json:"type"` 33 | Relationships map[string]interface{} `json:"relationships"` 34 | } 35 | 36 | // QueryParams that can be specified on all resource list calls. 37 | type QueryParams struct { 38 | PageLength int `url:"page_length"` 39 | Page int `url:"page"` 40 | Query string `url:"q"` 41 | Sort string `url:"sort"` 42 | } 43 | 44 | // JWErrorResponse represents a V2 Platform error response. 45 | type JWErrorResponse struct { 46 | Errors []JWError `json:"errors"` 47 | StatusCode int 48 | } 49 | 50 | // JWError represents a single error from the V2 Platform API. 51 | type JWError struct { 52 | Code string `json:"code"` 53 | Description string `json:"description"` 54 | } 55 | 56 | type rawError struct { 57 | Error *JWErrorResponse 58 | } 59 | 60 | // Error serializes the error object to JSON and returns it as a string. 61 | func (e *JWErrorResponse) Error() string { 62 | ret, err := json.Marshal(e) 63 | if err != nil { 64 | errorMsg := fmt.Sprintf("Unknown error when parsing JSON response: %s", err) 65 | return errorMsg 66 | } 67 | return string(ret) 68 | } 69 | 70 | // NewV2Client creates an authenticated V2 Client. 71 | func NewV2Client(authToken string) *V2Client { 72 | return &V2Client{ 73 | Version: version, 74 | authToken: authToken, 75 | baseURL: &url.URL{ 76 | Scheme: "https", 77 | Host: apiHost, 78 | }, 79 | client: http.DefaultClient, 80 | } 81 | } 82 | 83 | // Request performs an authenticated HTTP request to the V2 Platform API. 84 | func (c *V2Client) Request(method, path string, response interface{}, data interface{}, queryParams url.Values) error { 85 | var err error 86 | requestURL, err := c.urlFromPath(path) 87 | 88 | if queryParams != nil { 89 | requestURL.RawQuery = queryParams.Encode() 90 | } 91 | 92 | payload := []byte{} 93 | if data != nil { 94 | payload, _ = json.Marshal(data) 95 | } 96 | 97 | request, err := http.NewRequest(method, requestURL.String(), bytes.NewBuffer(payload)) 98 | request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.authToken)) 99 | request.Header.Set("User-Agent", fmt.Sprintf("jwplatform-go/%s", c.Version)) 100 | 101 | err = c.Do(request, &response) 102 | return err 103 | } 104 | 105 | // Do executes the request and parses V2 Platform API errors. 106 | func (c *V2Client) Do(req *http.Request, v interface{}) error { 107 | var resp *http.Response 108 | var err error 109 | 110 | resp, err = c.client.Do(req) 111 | if err != nil { 112 | return err 113 | } 114 | defer resp.Body.Close() 115 | 116 | if resp.StatusCode >= 400 { 117 | var raw rawError 118 | var e JWErrorResponse 119 | if err := json.NewDecoder(resp.Body).Decode(&e); err != nil { 120 | return err 121 | } 122 | raw.Error = &e 123 | raw.Error.StatusCode = resp.StatusCode 124 | return raw.Error 125 | } 126 | 127 | err = json.NewDecoder(resp.Body).Decode(v) 128 | switch { 129 | case err == io.EOF: 130 | return nil 131 | case err != nil: 132 | return err 133 | } 134 | return err 135 | } 136 | 137 | func (c *V2Client) urlFromPath(path string) (*url.URL, error) { 138 | url, e := url.Parse(path) 139 | absoluteURL := c.baseURL.ResolveReference(url) 140 | return absoluteURL, e 141 | } 142 | -------------------------------------------------------------------------------- /api_test.go: -------------------------------------------------------------------------------- 1 | package jwplatform 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "gopkg.in/h2non/gock.v1" 10 | ) 11 | 12 | 13 | func TestUrlFromPath(t *testing.T) { 14 | client := NewV2Client("authToken") 15 | path := "/v2/sites/abc/media/123" 16 | expected := "https://api.jwplayer.com/v2/sites/abc/media/123" 17 | resultURL, _ := client.urlFromPath(path) 18 | assert.Equal(t, resultURL.String(), expected) 19 | } 20 | 21 | func TestInvalidBody(t *testing.T) { 22 | defer gock.Off() 23 | 24 | siteID := "abcdefgh" 25 | mediaID := "mnbvcxkj" 26 | mockAuthToken := "shhh" 27 | errorCode := "invalid_body" 28 | errorDescription := "'name' is too long" 29 | 30 | requestPath := fmt.Sprintf("/v2/sites/%s/media/%s", siteID, mediaID) 31 | jwErrors := []JWError{ 32 | JWError{ 33 | Code: errorCode, 34 | Description: errorDescription, 35 | }} 36 | jwResponseError := &JWErrorResponse{ 37 | Errors: jwErrors, 38 | } 39 | 40 | gock.New("https://api.jwplayer.com"). 41 | Get(requestPath). 42 | MatchHeader("Authorization", "^Bearer .+"). 43 | MatchHeader("User-Agent", "^jwplatform-go/+"). 44 | Reply(400). 45 | JSON(jwResponseError) 46 | 47 | testClient := New(mockAuthToken) 48 | _, err := testClient.Media.Get(siteID, mediaID) 49 | assert.Error(t, err) 50 | jwErr := err.(*JWErrorResponse) 51 | assert.Equal(t, 400, jwErr.StatusCode) 52 | jwError := jwErr.Errors[0] 53 | assert.Equal(t, errorCode, jwError.Code) 54 | assert.Equal(t, errorDescription, jwError.Description) 55 | } 56 | 57 | func TestNotFound(t *testing.T) { 58 | defer gock.Off() 59 | 60 | siteID := "abcdefgh" 61 | mediaID := "mnbvcxkj" 62 | mockAuthToken := "shhh" 63 | errorCode := "not_found" 64 | errorDescription := "The requested resource could not be found." 65 | 66 | requestPath := fmt.Sprintf("/v2/sites/%s/media/%s", siteID, mediaID) 67 | response := map[string]interface{}{ 68 | "errors": []interface{}{ 69 | map[string]string{ 70 | "code": errorCode, 71 | "description": errorDescription, 72 | }, 73 | }, 74 | } 75 | 76 | gock.New("https://api.jwplayer.com"). 77 | Get(requestPath). 78 | MatchHeader("Authorization", "^Bearer .+"). 79 | MatchHeader("User-Agent", "^jwplatform-go/+"). 80 | Reply(404). 81 | JSON(response) 82 | 83 | testClient := New(mockAuthToken) 84 | _, err := testClient.Media.Get(siteID, mediaID) 85 | assert.Error(t, err) 86 | jwErr := err.(*JWErrorResponse) 87 | assert.Equal(t, 404, jwErr.StatusCode) 88 | jwError := jwErr.Errors[0] 89 | assert.Equal(t, errorCode, jwError.Code) 90 | assert.Equal(t, errorDescription, jwError.Description) 91 | } 92 | 93 | func TestUnauthorized(t *testing.T) { 94 | defer gock.Off() 95 | 96 | siteID := "abcdefgh" 97 | mediaID := "mnbvcxkj" 98 | mockAuthToken := "shhh" 99 | errorCode := "unauthorized" 100 | errorDescription := "Unauthorized" 101 | 102 | requestPath := fmt.Sprintf("/v2/sites/%s/media/%s", siteID, mediaID) 103 | response := map[string]interface{}{ 104 | "errors": []interface{}{ 105 | map[string]string{ 106 | "code": errorCode, 107 | "description": errorDescription, 108 | }, 109 | }, 110 | } 111 | 112 | gock.New("https://api.jwplayer.com"). 113 | Get(requestPath). 114 | MatchHeader("Authorization", "^Bearer .+"). 115 | MatchHeader("User-Agent", "^jwplatform-go/+"). 116 | Reply(403). 117 | JSON(response) 118 | 119 | testClient := New(mockAuthToken) 120 | _, err := testClient.Media.Get(siteID, mediaID) 121 | assert.Error(t, err) 122 | jwErr := err.(*JWErrorResponse) 123 | assert.Equal(t, 403, jwErr.StatusCode) 124 | jwError := jwErr.Errors[0] 125 | assert.Equal(t, errorCode, jwError.Code) 126 | assert.Equal(t, errorDescription, jwError.Description) 127 | } 128 | 129 | func TestJWErrorHandling(t *testing.T) { 130 | defer gock.Off() 131 | 132 | siteID := "abcdefgh" 133 | mediaID := "mnbvcxkj" 134 | mockAuthToken := "shhh" 135 | errorCode := "invalid_body" 136 | errorDescription := "'name' is too long" 137 | 138 | requestPath := fmt.Sprintf("/v2/sites/%s/media/%s", siteID, mediaID) 139 | jwErrors := []JWError{ 140 | JWError{ 141 | Code: errorCode, 142 | Description: errorDescription, 143 | }} 144 | jwResponseError := &JWErrorResponse{ 145 | Errors: jwErrors, 146 | } 147 | 148 | gock.New("https://api.jwplayer.com"). 149 | Get(requestPath). 150 | MatchHeader("Authorization", "^Bearer .+"). 151 | MatchHeader("User-Agent", "^jwplatform-go/+"). 152 | Reply(400). 153 | JSON(jwResponseError) 154 | 155 | testClient := New(mockAuthToken) 156 | _, err := testClient.Media.Get(siteID, mediaID) 157 | assert.Error(t, err) 158 | jwErr := err.(*JWErrorResponse) 159 | assert.Equal(t, 400, jwErr.StatusCode) 160 | jwError := jwErr.Errors[0] 161 | assert.Equal(t, errorCode, jwError.Code) 162 | assert.Equal(t, errorDescription, jwError.Description) 163 | } 164 | 165 | func TestUnmarshalResourceResponse(t *testing.T) { 166 | resourceResponseData := map[string]interface{}{ 167 | "id": "9jTnCiPO", 168 | "type": "resource_type", 169 | "created": "2019-09-25T15:29:11.042095+00:00", 170 | "last_modified": "2019-09-25T15:29:11.042095+00:00", 171 | "relationships": map[string]interface{}{ 172 | "protectionrule": map[string]string{ 173 | "id": "protectionrule_id", 174 | }, 175 | }, 176 | } 177 | 178 | bytes, err := json.Marshal(&resourceResponseData) 179 | assert.NoError(t, err) 180 | 181 | var resourceResponse V2ResourceResponse 182 | err = json.Unmarshal(bytes, &resourceResponse) 183 | assert.NoError(t, err) 184 | 185 | assert.Equal(t, "9jTnCiPO", resourceResponse.ID) 186 | assert.Equal(t, "resource_type", resourceResponse.Type) 187 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", resourceResponse.Created) 188 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", resourceResponse.LastModified) 189 | assert.Equal(t, map[string]interface{}{"id": "protectionrule_id"}, resourceResponse.Relationships["protectionrule"]) 190 | } 191 | 192 | func TestUnmarshalResourcesResponse(t *testing.T) { 193 | resourcesResponseData := map[string]int{ 194 | "page": 1, 195 | "page_length": 10, 196 | "total": 541, 197 | } 198 | 199 | bytes, err := json.Marshal(&resourcesResponseData) 200 | assert.NoError(t, err) 201 | 202 | var resourcesResponse V2ResourcesResponse 203 | err = json.Unmarshal(bytes, &resourcesResponse) 204 | assert.NoError(t, err) 205 | 206 | assert.Equal(t, 541, resourcesResponse.Total) 207 | assert.Equal(t, 1, resourcesResponse.Page) 208 | assert.Equal(t, 10, resourcesResponse.PageLength) 209 | } 210 | 211 | func TestUnmarshalJWErrorResponse(t *testing.T) { 212 | errorResponseData := map[string]interface{}{ 213 | "errors": []interface{}{ 214 | map[string]string{ 215 | "code": "invalid_body", 216 | "description": "name was too long", 217 | }, 218 | }, 219 | } 220 | 221 | bytes, err := json.Marshal(&errorResponseData) 222 | assert.NoError(t, err) 223 | 224 | var errorResponse JWErrorResponse 225 | err = json.Unmarshal(bytes, &errorResponse) 226 | assert.NoError(t, err) 227 | 228 | assert.Equal(t, "invalid_body", errorResponse.Errors[0].Code) 229 | assert.Equal(t, "name was too long", errorResponse.Errors[0].Description) 230 | } 231 | -------------------------------------------------------------------------------- /channels.go: -------------------------------------------------------------------------------- 1 | package jwplatform 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/google/go-querystring/query" 8 | ) 9 | 10 | // ChannelResource is the resource that is returned for all Channel resource requests 11 | type ChannelResource struct { 12 | V2ResourceResponse 13 | 14 | Metadata ChannelMetadata `json:"metadata"` 15 | Latency string `json:"latency"` 16 | RecentEvents []recentEvent `json:"recent_events"` 17 | ReconnectWindow int `json:"reconnect_window"` 18 | Status string `json:"status"` 19 | StreamKey string `json:"stream_key"` 20 | } 21 | 22 | type recentEvent struct { 23 | MediaID string `json:"media_id"` 24 | Status string `json:"status"` 25 | } 26 | 27 | // ChannelCreateRequest is the request structure required for Channel create calls. 28 | type ChannelCreateRequest struct { 29 | Metadata ChannelCreateMetadata `json:"metadata"` 30 | } 31 | 32 | // ChannelUpdateRequest is the request structure required for Channel update calls. 33 | type ChannelUpdateRequest struct { 34 | Metadata ChannelMetadata `json:"metadata"` 35 | } 36 | 37 | // ChannelMetadata describes a Channel resource 38 | type ChannelMetadata struct { 39 | CustomParams map[string]string `json:"custom_params"` 40 | Dvr string `json:"dvr"` 41 | SimulcastTargets []simulcastTarget `json:"simulcast_targets"` 42 | Tags []string `json:"tags"` 43 | Title string `json:"title"` 44 | } 45 | 46 | type simulcastTarget struct { 47 | StreamKey string `json:"stream_key"` 48 | StreamURL string `json:"stream_url"` 49 | Title string `json:"title"` 50 | } 51 | 52 | // ChannelCreateMetadata describes the request structure used to create a Channel resource 53 | type ChannelCreateMetadata struct { 54 | CustomParams map[string]string `json:"custom_params"` 55 | Dvr string `json:"dvr"` 56 | SimulcastTargets []simulcastTarget `json:"simulcast_targets"` 57 | Tags []string `json:"tags"` 58 | Title string `json:"title"` 59 | Latency string `json:"latency"` 60 | ReconnectWindow int `json:"reconnect_window"` 61 | } 62 | 63 | // ChannelResourcesResponse is the response structure for Channel list calls. 64 | type ChannelResourcesResponse struct { 65 | V2ResourcesResponse 66 | 67 | Channels []ChannelResource `json:"channels"` 68 | } 69 | 70 | // ChannelsClient for interacting with V2 Channels and Channel Events API. 71 | type ChannelsClient struct { 72 | v2Client *V2Client 73 | Events *EventsClient 74 | } 75 | 76 | // NewChannelsClient returns a new Channels Client 77 | func NewChannelsClient(v2Client *V2Client) *ChannelsClient { 78 | return &ChannelsClient{ 79 | v2Client: v2Client, 80 | Events: &EventsClient{ 81 | v2Client: v2Client, 82 | }, 83 | } 84 | } 85 | 86 | // Get a single Channel resource by ID. 87 | func (c *ChannelsClient) Get(siteID, channelID string) (*ChannelResource, error) { 88 | channel := &ChannelResource{} 89 | path := fmt.Sprintf("/v2/sites/%s/channels/%s", siteID, channelID) 90 | err := c.v2Client.Request(http.MethodGet, path, channel, nil, nil) 91 | return channel, err 92 | } 93 | 94 | // Create a Channel resource. 95 | func (c *ChannelsClient) Create(siteID string, ChannelCreateMetadata *ChannelCreateMetadata) (*ChannelResource, error) { 96 | createRequestData := &ChannelCreateRequest{Metadata: *ChannelCreateMetadata} 97 | channel := &ChannelResource{} 98 | path := fmt.Sprintf("/v2/sites/%s/channels", siteID) 99 | err := c.v2Client.Request(http.MethodPost, path, channel, createRequestData, nil) 100 | return channel, err 101 | } 102 | 103 | // List all Channel resources associated with a given Site ID. 104 | func (c *ChannelsClient) List(siteID string, queryParams *QueryParams) (*ChannelResourcesResponse, error) { 105 | channels := &ChannelResourcesResponse{} 106 | path := fmt.Sprintf("/v2/sites/%s/channels", siteID) 107 | urlValues, _ := query.Values(queryParams) 108 | err := c.v2Client.Request(http.MethodGet, path, channels, nil, urlValues) 109 | return channels, err 110 | } 111 | 112 | // Update a Channel resource by ID. 113 | func (c *ChannelsClient) Update(siteID, channelID string, channelMetadata *ChannelMetadata) (*ChannelResource, error) { 114 | updateRequestData := &ChannelUpdateRequest{Metadata: *channelMetadata} 115 | channel := &ChannelResource{} 116 | path := fmt.Sprintf("/v2/sites/%s/channels/%s", siteID, channelID) 117 | err := c.v2Client.Request(http.MethodPatch, path, channel, updateRequestData, nil) 118 | return channel, err 119 | } 120 | 121 | // Delete a Channel resource by ID. 122 | func (c *ChannelsClient) Delete(siteID, channelID string) error { 123 | path := fmt.Sprintf("/v2/sites/%s/channels/%s", siteID, channelID) 124 | err := c.v2Client.Request(http.MethodDelete, path, nil, nil, nil) 125 | return err 126 | } 127 | -------------------------------------------------------------------------------- /channels_test.go: -------------------------------------------------------------------------------- 1 | package jwplatform 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "gopkg.in/h2non/gock.v1" 11 | ) 12 | 13 | func TestGetChannel(t *testing.T) { 14 | defer gock.Off() 15 | 16 | siteID := "abcdefgh" 17 | channelID := "mnbvcxkj" 18 | mockAuthToken := "shhh" 19 | 20 | requestPath := fmt.Sprintf("/v2/sites/%s/channels/%s", siteID, channelID) 21 | mockChannelResponse := map[string]string{"id": channelID} 22 | 23 | gock.New("https://api.jwplayer.com"). 24 | Get(requestPath). 25 | MatchHeader("Authorization", "^Bearer .+"). 26 | MatchHeader("User-Agent", "^jwplatform-go/+"). 27 | Reply(200). 28 | JSON(mockChannelResponse) 29 | 30 | testClient := New(mockAuthToken) 31 | channel, err := testClient.Channels.Get(siteID, channelID) 32 | assert.Equal(t, channelID, channel.ID) 33 | assert.Equal(t, nil, err) 34 | } 35 | 36 | func TestDeleteChannel(t *testing.T) { 37 | defer gock.Off() 38 | 39 | siteID := "abcdefgh" 40 | channelID := "mnbvcxkj" 41 | mockAuthToken := "shhh" 42 | 43 | requestPath := fmt.Sprintf("/v2/sites/%s/channels/%s", siteID, channelID) 44 | 45 | gock.New("https://api.jwplayer.com"). 46 | Delete(requestPath). 47 | MatchHeader("Authorization", "^Bearer .+"). 48 | MatchHeader("User-Agent", "^jwplatform-go/+"). 49 | Reply(204) 50 | 51 | testClient := New(mockAuthToken) 52 | err := testClient.Channels.Delete(siteID, channelID) 53 | assert.Equal(t, nil, err) 54 | } 55 | 56 | func TestCreateChannel(t *testing.T) { 57 | defer gock.Off() 58 | 59 | siteID := "abcdefgh" 60 | channelID := "mnbvcxkj" 61 | mockAuthToken := "shhh" 62 | 63 | requestPath := fmt.Sprintf("/v2/sites/%s/channels", siteID) 64 | mockChannelResponse := map[string]string{"id": channelID} 65 | 66 | gock.New("https://api.jwplayer.com"). 67 | Post(requestPath). 68 | MatchHeader("Authorization", "^Bearer .+"). 69 | MatchHeader("User-Agent", "^jwplatform-go/+"). 70 | Reply(201). 71 | JSON(mockChannelResponse) 72 | 73 | testClient := New(mockAuthToken) 74 | newChannel := &ChannelCreateMetadata{Title: "Another test video"} 75 | channel, err := testClient.Channels.Create(siteID, newChannel) 76 | assert.Equal(t, channelID, channel.ID) 77 | assert.Equal(t, nil, err) 78 | } 79 | 80 | func TestUpdateChannel(t *testing.T) { 81 | defer gock.Off() 82 | 83 | siteID := "abcdefgh" 84 | channelID := "mnbvcxkj" 85 | mockAuthToken := "shhh" 86 | 87 | requestPath := fmt.Sprintf("/v2/sites/%s/channels/%s", siteID, channelID) 88 | mockChannelResponse := map[string]string{"id": channelID} 89 | 90 | gock.New("https://api.jwplayer.com"). 91 | Patch(requestPath). 92 | MatchHeader("Authorization", "^Bearer .+"). 93 | MatchHeader("User-Agent", "^jwplatform-go/+"). 94 | Reply(200). 95 | JSON(mockChannelResponse) 96 | 97 | testClient := New(mockAuthToken) 98 | updateMetadata := &ChannelMetadata{Title: "Updating another test media"} 99 | channel, err := testClient.Channels.Update(siteID, channelID, updateMetadata) 100 | assert.Equal(t, channelID, channel.ID) 101 | assert.Equal(t, nil, err) 102 | } 103 | 104 | func TestListChannel(t *testing.T) { 105 | defer gock.Off() 106 | 107 | siteID := "abcdefgh" 108 | channelID := "aamkdcaz" 109 | mockAuthToken := "shhh" 110 | page := 2 111 | pageLength := 4 112 | 113 | requestPath := fmt.Sprintf("/v2/sites/%s/channels", siteID) 114 | mockChannelsResponse := map[string]interface{}{ 115 | "page_length": pageLength, 116 | "page": page, 117 | "channels": []map[string]string{{"id": channelID}}, 118 | } 119 | 120 | gock.New("https://api.jwplayer.com"). 121 | Get(requestPath). 122 | MatchHeader("Authorization", "^Bearer .+"). 123 | MatchHeader("User-Agent", "^jwplatform-go/+"). 124 | MatchParam("page", strconv.Itoa(page)). 125 | MatchParam("page_length", strconv.Itoa(pageLength)). 126 | Reply(200). 127 | JSON(mockChannelsResponse) 128 | 129 | testClient := New(mockAuthToken) 130 | params := &QueryParams{PageLength: pageLength, Page: page} 131 | channelsResponse, err := testClient.Channels.List(siteID, params) 132 | assert.Equal(t, page, channelsResponse.Page) 133 | assert.Equal(t, pageLength, channelsResponse.PageLength) 134 | assert.Equal(t, channelID, channelsResponse.Channels[0].ID) 135 | assert.Equal(t, nil, err) 136 | } 137 | 138 | func TestUnmarshalChannel(t *testing.T) { 139 | channelData := map[string]interface{}{ 140 | "id": "apmzKzSf", 141 | "type": "channel", 142 | "created": "2019-09-25T15:29:11.042095+00:00", 143 | "last_modified": "2019-09-25T15:29:11.042095+00:00", 144 | "status": "idle", 145 | "reconnect_window": 60, 146 | "stream_key": "a_stream_key", 147 | "metadata": map[string]interface{}{ 148 | "title": "my channel", 149 | "dvr": "on", 150 | "simulcast_targets": []interface{}{ 151 | map[string]string{ 152 | "stream_key": "b_stream_key", 153 | "stream_url": "http://streamurl.com", 154 | "title": "target title", 155 | }, 156 | }, 157 | "tags": []string{"tag_a", "tag_b"}, 158 | "custom_params": map[string]string{ 159 | "key": "value", 160 | }, 161 | }, 162 | "latency": "default", 163 | "recent_events": []interface{}{ 164 | map[string]string{ 165 | "media_id": "a_media_id", 166 | "status": "idle", 167 | }, 168 | }, 169 | } 170 | 171 | bytes, err := json.Marshal(&channelData) 172 | assert.NoError(t, err) 173 | 174 | var channelResource ChannelResource 175 | err = json.Unmarshal(bytes, &channelResource) 176 | assert.NoError(t, err) 177 | 178 | assert.Equal(t, "apmzKzSf", channelResource.ID) 179 | assert.Equal(t, "channel", channelResource.Type) 180 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", channelResource.Created) 181 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", channelResource.LastModified) 182 | 183 | assert.Equal(t, "default", channelResource.Latency) 184 | assert.Equal(t, "idle", channelResource.Status) 185 | assert.Equal(t, "a_stream_key", channelResource.StreamKey) 186 | assert.Equal(t, 60, channelResource.ReconnectWindow) 187 | 188 | assert.Equal(t, "a_media_id", channelResource.RecentEvents[0].MediaID) 189 | assert.Equal(t, "idle", channelResource.RecentEvents[0].Status) 190 | 191 | assert.Equal(t, "my channel", channelResource.Metadata.Title) 192 | assert.Equal(t, "on", channelResource.Metadata.Dvr) 193 | assert.Equal(t, []string{"tag_a", "tag_b"}, channelResource.Metadata.Tags) 194 | assert.Equal(t, map[string]string{"key": "value"}, channelResource.Metadata.CustomParams) 195 | 196 | assert.Equal(t, "b_stream_key", channelResource.Metadata.SimulcastTargets[0].StreamKey) 197 | assert.Equal(t, "http://streamurl.com", channelResource.Metadata.SimulcastTargets[0].StreamURL) 198 | assert.Equal(t, "target title", channelResource.Metadata.SimulcastTargets[0].Title) 199 | } 200 | -------------------------------------------------------------------------------- /drm_policies.go: -------------------------------------------------------------------------------- 1 | package jwplatform 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/google/go-querystring/query" 8 | ) 9 | 10 | // DRMPolicyResource is the resource that is returned for all DRM Policy resource requests 11 | type DRMPolicyResource struct { 12 | V2ResourceResponse 13 | Metadata DRMPolicyMetadata `json:"metadata"` 14 | } 15 | 16 | // DRMPolicyWriteRequest is the request structure required for DRMPolicy create and update calls. 17 | type DRMPolicyWriteRequest struct { 18 | Metadata DRMPolicyMetadata `json:"metadata"` 19 | } 20 | 21 | // DRMPolicyMetadata describes a DRMPolicy resource 22 | type DRMPolicyMetadata struct { 23 | Name string `json:"name"` 24 | MaxWidth int `json:"max_width"` 25 | WidevineSecurity string `json:"widevine_security"` 26 | PlayreadySecurity int `json:"playready_security"` 27 | AllowOfflinePersistence bool `json:"allow_offline_persistence"` 28 | DigitalOutputProtection string `json:"digital_output_protection"` 29 | LicenseDuration int `json:"license_duration"` 30 | PlaybackDuration int `json:"playback_duration"` 31 | } 32 | 33 | // DRMPolicyResourcesResponse is the response structure for DRMPolicy list calls. 34 | type DRMPolicyResourcesResponse struct { 35 | V2ResourcesResponse 36 | 37 | DRMPolicies []DRMPolicyResource `json:"drm_policies"` 38 | } 39 | 40 | // DRMPoliciesClient for interacting with V2 DRM Policies API. 41 | type DRMPoliciesClient struct { 42 | v2Client *V2Client 43 | } 44 | 45 | // Get a single DRMPolicy resource by ID. 46 | func (c *DRMPoliciesClient) Get(siteID, drmPolicyID string) (*DRMPolicyResource, error) { 47 | drmPolicy := &DRMPolicyResource{} 48 | path := fmt.Sprintf("/v2/sites/%s/drm_policies/%s", siteID, drmPolicyID) 49 | err := c.v2Client.Request(http.MethodGet, path, drmPolicy, nil, nil) 50 | return drmPolicy, err 51 | } 52 | 53 | // Create a DRMPolicy resource. 54 | func (c *DRMPoliciesClient) Create(siteID string, DRMPolicyMetadata *DRMPolicyMetadata) (*DRMPolicyResource, error) { 55 | createRequestData := &DRMPolicyWriteRequest{Metadata: *DRMPolicyMetadata} 56 | drmPolicy := &DRMPolicyResource{} 57 | path := fmt.Sprintf("/v2/sites/%s/drm_policies", siteID) 58 | err := c.v2Client.Request(http.MethodPost, path, drmPolicy, createRequestData, nil) 59 | return drmPolicy, err 60 | } 61 | 62 | // List all DRMPolicy resources. 63 | func (c *DRMPoliciesClient) List(siteID string, queryParams *QueryParams) (*DRMPolicyResourcesResponse, error) { 64 | drmPolicies := &DRMPolicyResourcesResponse{} 65 | path := fmt.Sprintf("/v2/sites/%s/drm_policies", siteID) 66 | urlValues, _ := query.Values(queryParams) 67 | err := c.v2Client.Request(http.MethodGet, path, drmPolicies, nil, urlValues) 68 | return drmPolicies, err 69 | } 70 | 71 | // Update a DRMPolicy resource by ID. 72 | func (c *DRMPoliciesClient) Update(siteID, drmPolicyID string, DRMPolicyMetadata *DRMPolicyMetadata) (*DRMPolicyResource, error) { 73 | updateRequestData := &DRMPolicyWriteRequest{Metadata: *DRMPolicyMetadata} 74 | drmPolicy := &DRMPolicyResource{} 75 | path := fmt.Sprintf("/v2/sites/%s/drm_policies/%s", siteID, drmPolicyID) 76 | err := c.v2Client.Request(http.MethodPatch, path, drmPolicy, updateRequestData, nil) 77 | return drmPolicy, err 78 | } 79 | 80 | // Delete a DRMPolicy resource by ID. 81 | func (c *DRMPoliciesClient) Delete(siteID, drmPolicyID string) error { 82 | path := fmt.Sprintf("/v2/sites/%s/drm_policies/%s", siteID, drmPolicyID) 83 | err := c.v2Client.Request(http.MethodDelete, path, nil, nil, nil) 84 | return err 85 | } 86 | -------------------------------------------------------------------------------- /drm_policies_test.go: -------------------------------------------------------------------------------- 1 | package jwplatform 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "gopkg.in/h2non/gock.v1" 11 | ) 12 | 13 | func TestGetDRMPolicy(t *testing.T) { 14 | defer gock.Off() 15 | 16 | siteID := "abcdefgh" 17 | drmPolicyID := "mnbvcxkj" 18 | mockAuthToken := "shhh" 19 | 20 | requestPath := fmt.Sprintf("/v2/sites/%s/drm_policies/%s", siteID, drmPolicyID) 21 | mockDRMPolicyResponse := map[string]string{"id": drmPolicyID} 22 | 23 | gock.New("https://api.jwplayer.com"). 24 | Get(requestPath). 25 | MatchHeader("Authorization", "^Bearer .+"). 26 | MatchHeader("User-Agent", "^jwplatform-go/+"). 27 | Reply(200). 28 | JSON(mockDRMPolicyResponse) 29 | 30 | testClient := New(mockAuthToken) 31 | drmPolicy, err := testClient.DRMPolicies.Get(siteID, drmPolicyID) 32 | assert.Equal(t, drmPolicyID, drmPolicy.ID) 33 | assert.Equal(t, nil, err) 34 | } 35 | 36 | func TestDeleteDRMPolicy(t *testing.T) { 37 | defer gock.Off() 38 | 39 | siteID := "abcdefgh" 40 | drmPolicyID := "mnbvcxkj" 41 | mockAuthToken := "shhh" 42 | 43 | requestPath := fmt.Sprintf("/v2/sites/%s/drm_policies/%s", siteID, drmPolicyID) 44 | 45 | gock.New("https://api.jwplayer.com"). 46 | Delete(requestPath). 47 | MatchHeader("Authorization", "^Bearer .+"). 48 | MatchHeader("User-Agent", "^jwplatform-go/+"). 49 | Reply(204) 50 | 51 | testClient := New(mockAuthToken) 52 | err := testClient.DRMPolicies.Delete(siteID, drmPolicyID) 53 | assert.Equal(t, nil, err) 54 | } 55 | 56 | func TestCreateDRMPolicy(t *testing.T) { 57 | defer gock.Off() 58 | 59 | siteID := "abcdefgh" 60 | drmPolicyID := "mnbvcxkj" 61 | mockAuthToken := "shhh" 62 | 63 | requestPath := fmt.Sprintf("/v2/sites/%s/drm_policies", siteID) 64 | mockDRMPolicyResponse := map[string]string{"id": drmPolicyID} 65 | 66 | gock.New("https://api.jwplayer.com"). 67 | Post(requestPath). 68 | MatchHeader("Authorization", "^Bearer .+"). 69 | MatchHeader("User-Agent", "^jwplatform-go/+"). 70 | Reply(201). 71 | JSON(mockDRMPolicyResponse) 72 | 73 | testClient := New(mockAuthToken) 74 | newDRMPolicy := &DRMPolicyMetadata{Name: "My DRM Policy"} 75 | drmPolicy, err := testClient.DRMPolicies.Create(siteID, newDRMPolicy) 76 | assert.Equal(t, drmPolicyID, drmPolicy.ID) 77 | assert.Equal(t, nil, err) 78 | } 79 | 80 | func TestUpdateDRMPolicy(t *testing.T) { 81 | defer gock.Off() 82 | 83 | siteID := "abcdefgh" 84 | drmPolicyID := "mnbvcxkj" 85 | mockAuthToken := "shhh" 86 | 87 | requestPath := fmt.Sprintf("/v2/sites/%s/drm_policies/%s", siteID, drmPolicyID) 88 | mockDRMPolicyResponse := map[string]string{"id": drmPolicyID} 89 | 90 | gock.New("https://api.jwplayer.com"). 91 | Patch(requestPath). 92 | MatchHeader("Authorization", "^Bearer .+"). 93 | MatchHeader("User-Agent", "^jwplatform-go/+"). 94 | Reply(200). 95 | JSON(mockDRMPolicyResponse) 96 | 97 | testClient := New(mockAuthToken) 98 | updateMetadata := &DRMPolicyMetadata{Name: "My Updated DRM Policy"} 99 | drmPolicy, err := testClient.DRMPolicies.Update(siteID, drmPolicyID, updateMetadata) 100 | assert.Equal(t, drmPolicyID, drmPolicy.ID) 101 | assert.Equal(t, nil, err) 102 | } 103 | 104 | func TestListDRMPolicy(t *testing.T) { 105 | defer gock.Off() 106 | 107 | siteID := "abcdefgh" 108 | drmPolicyID := "aamkdcaz" 109 | mockAuthToken := "shhh" 110 | page := 2 111 | pageLength := 4 112 | 113 | requestPath := fmt.Sprintf("/v2/sites/%s/drm_policies", siteID) 114 | mockDRMPolicysResponse := map[string]interface{}{ 115 | "page_length": pageLength, 116 | "page": page, 117 | "drm_policies": []map[string]string{{"id": drmPolicyID}}, 118 | } 119 | 120 | gock.New("https://api.jwplayer.com"). 121 | Get(requestPath). 122 | MatchHeader("Authorization", "^Bearer .+"). 123 | MatchHeader("User-Agent", "^jwplatform-go/+"). 124 | MatchParam("page", strconv.Itoa(page)). 125 | MatchParam("page_length", strconv.Itoa(pageLength)). 126 | Reply(200). 127 | JSON(mockDRMPolicysResponse) 128 | 129 | testClient := New(mockAuthToken) 130 | params := &QueryParams{PageLength: pageLength, Page: page} 131 | drmPolicysResponse, err := testClient.DRMPolicies.List(siteID, params) 132 | assert.Equal(t, page, drmPolicysResponse.Page) 133 | assert.Equal(t, pageLength, drmPolicysResponse.PageLength) 134 | assert.Equal(t, drmPolicyID, drmPolicysResponse.DRMPolicies[0].ID) 135 | assert.Equal(t, nil, err) 136 | } 137 | 138 | func TestUnmarshaDrmPolicy(t *testing.T) { 139 | drmPolicyData := map[string]interface{}{ 140 | "id": "OiUUoa90", 141 | "type": "drm_policy", 142 | "created": "2019-09-25T15:29:11.042095+00:00", 143 | "last_modified": "2019-09-25T15:29:11.042095+00:00", 144 | "metadata": map[string]interface{}{ 145 | "name": "my drm", 146 | "max_width": 1680, 147 | "widevine_security": "sw_secure_crypto", 148 | "playready_security": 3000, 149 | "allow_offline_persisteence": false, 150 | "digital_output_protection": "not_required", 151 | "license_duration": 12600, 152 | "playback_duration": 24600, 153 | }, 154 | } 155 | 156 | bytes, err := json.Marshal(&drmPolicyData) 157 | assert.NoError(t, err) 158 | 159 | var drmPolicyResource DRMPolicyResource 160 | err = json.Unmarshal(bytes, &drmPolicyResource) 161 | assert.NoError(t, err) 162 | 163 | assert.Equal(t, "OiUUoa90", drmPolicyResource.ID) 164 | assert.Equal(t, "drm_policy", drmPolicyResource.Type) 165 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", drmPolicyResource.Created) 166 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", drmPolicyResource.LastModified) 167 | 168 | assert.Equal(t, "my drm", drmPolicyResource.Metadata.Name) 169 | assert.Equal(t, 1680, drmPolicyResource.Metadata.MaxWidth) 170 | assert.Equal(t, "sw_secure_crypto", drmPolicyResource.Metadata.WidevineSecurity) 171 | assert.Equal(t, 3000, drmPolicyResource.Metadata.PlayreadySecurity) 172 | assert.Equal(t, false, drmPolicyResource.Metadata.AllowOfflinePersistence) 173 | assert.Equal(t, "not_required", drmPolicyResource.Metadata.DigitalOutputProtection) 174 | assert.Equal(t, 12600, drmPolicyResource.Metadata.LicenseDuration) 175 | assert.Equal(t, 24600, drmPolicyResource.Metadata.PlaybackDuration) 176 | } 177 | -------------------------------------------------------------------------------- /events.go: -------------------------------------------------------------------------------- 1 | package jwplatform 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/google/go-querystring/query" 8 | ) 9 | 10 | // EventResource is the resource that is returned for all Event resource requests, 11 | // with the exception of the Create action, which extends this struct with upload-related data. 12 | type EventResource struct { 13 | V2ResourceResponse 14 | MasterAccess masterAccess `json:"master_access"` 15 | MediaID string `json:"media_id"` 16 | Status string `json:"status"` 17 | } 18 | 19 | type masterAccess struct { 20 | Status string `json:"status"` 21 | Expiration string `json:"expiration"` 22 | } 23 | 24 | // EventResourcesResponse is the response structure for Event list calls. 25 | type EventResourcesResponse struct { 26 | V2ResourcesResponse 27 | Events []EventResource `json:"events"` 28 | } 29 | 30 | // EventsClient for interacting with V2 Events API. 31 | type EventsClient struct { 32 | v2Client *V2Client 33 | } 34 | 35 | // Get a single Event resource by Channel and Event ID. 36 | func (c *EventsClient) Get(siteID, channelID, eventID string) (*EventResource, error) { 37 | channel := &EventResource{} 38 | path := fmt.Sprintf("/v2/sites/%s/channels/%s/events/%s", siteID, channelID, eventID) 39 | err := c.v2Client.Request(http.MethodGet, path, channel, nil, nil) 40 | return channel, err 41 | } 42 | 43 | // List all Event resources associated with a given Site and Channel ID. 44 | func (c *EventsClient) List(siteID, channelID string, queryParams *QueryParams) (*EventResourcesResponse, error) { 45 | channels := &EventResourcesResponse{} 46 | path := fmt.Sprintf("/v2/sites/%s/channels/%s/events", siteID, channelID) 47 | urlValues, _ := query.Values(queryParams) 48 | err := c.v2Client.Request(http.MethodGet, path, channels, nil, urlValues) 49 | return channels, err 50 | } 51 | 52 | // RequestMaster reqyests the master asset resources associated with a given Site ID. 53 | func (c *EventsClient) RequestMaster(siteID, channelID, eventID string) error { 54 | path := fmt.Sprintf("/v2/sites/%s/channels/%s/events/%s/request_master", siteID, channelID, eventID) 55 | err := c.v2Client.Request(http.MethodPut, path, nil, nil, nil) 56 | return err 57 | } 58 | -------------------------------------------------------------------------------- /events_test.go: -------------------------------------------------------------------------------- 1 | package jwplatform 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "gopkg.in/h2non/gock.v1" 10 | ) 11 | 12 | func TestGetEvents(t *testing.T) { 13 | defer gock.Off() 14 | 15 | siteID := "abcdefgh" 16 | channelID := "mnbvcxkj" 17 | eventID := "mnbvcxkj" 18 | mockAuthToken := "shhh" 19 | 20 | requestPath := fmt.Sprintf("/v2/sites/%s/channels/%s/events/%s", siteID, channelID, eventID) 21 | mockEventResponse := map[string]string{"id": eventID} 22 | 23 | gock.New("https://api.jwplayer.com"). 24 | Get(requestPath). 25 | MatchHeader("Authorization", "^Bearer .+"). 26 | MatchHeader("User-Agent", "^jwplatform-go/+"). 27 | Reply(200). 28 | JSON(mockEventResponse) 29 | 30 | testClient := New(mockAuthToken) 31 | event, err := testClient.Channels.Events.Get(siteID, channelID, eventID) 32 | assert.Equal(t, eventID, event.ID) 33 | assert.Equal(t, nil, err) 34 | } 35 | 36 | func TestListEvents(t *testing.T) { 37 | defer gock.Off() 38 | 39 | siteID := "abcdefgh" 40 | channelID := "mnbvcxkj" 41 | eventID := "aqpozjv2a" 42 | page := 2 43 | pageLength := 4 44 | mockAuthToken := "shhh" 45 | 46 | requestPath := fmt.Sprintf("/v2/sites/%s/channels/%s/events", siteID, channelID) 47 | mockEventsResponse := map[string]interface{}{ 48 | "page_length": pageLength, 49 | "page": page, 50 | "events": []map[string]string{{"id": eventID}}, 51 | } 52 | 53 | gock.New("https://api.jwplayer.com"). 54 | Get(requestPath). 55 | MatchHeader("Authorization", "^Bearer .+"). 56 | MatchHeader("User-Agent", "^jwplatform-go/+"). 57 | Reply(200). 58 | JSON(mockEventsResponse) 59 | 60 | testClient := New(mockAuthToken) 61 | params := &QueryParams{PageLength: pageLength, Page: page} 62 | eventsListResponse, err := testClient.Channels.Events.List(siteID, channelID, params) 63 | assert.Equal(t, page, eventsListResponse.Page) 64 | assert.Equal(t, pageLength, eventsListResponse.PageLength) 65 | assert.Equal(t, eventID, eventsListResponse.Events[0].ID) 66 | assert.Equal(t, nil, err) 67 | } 68 | 69 | func TestRequestMaster(t *testing.T) { 70 | defer gock.Off() 71 | 72 | siteID := "abcdefgh" 73 | channelID := "mnbvcxkj" 74 | eventID := "mnbvcxkj" 75 | mockAuthToken := "shhh" 76 | 77 | requestPath := fmt.Sprintf("/v2/sites/%s/channels/%s/events/%s/request_master", siteID, channelID, eventID) 78 | 79 | gock.New("https://api.jwplayer.com"). 80 | Put(requestPath). 81 | MatchHeader("Authorization", "^Bearer .+"). 82 | MatchHeader("User-Agent", "^jwplatform-go/+"). 83 | Reply(204) 84 | 85 | testClient := New(mockAuthToken) 86 | err := testClient.Channels.Events.RequestMaster(siteID, channelID, eventID) 87 | assert.Equal(t, nil, err) 88 | } 89 | 90 | func TestUnmarshalEvent(t *testing.T) { 91 | eventData := map[string]interface{}{ 92 | "id": "tNvzjo7S", 93 | "type": "event", 94 | "created": "2019-09-25T15:29:11.042095+00:00", 95 | "last_modified": "2019-09-25T15:29:11.042095+00:00", 96 | "media_id": "a_media_id", 97 | "status": "idle", 98 | "master_access": map[string]string{ 99 | "status": "available", 100 | "expiration": "2022-11-11T07:50:00+00:00", 101 | }, 102 | } 103 | 104 | bytes, err := json.Marshal(&eventData) 105 | assert.NoError(t, err) 106 | 107 | var event EventResource 108 | err = json.Unmarshal(bytes, &event) 109 | assert.NoError(t, err) 110 | 111 | assert.Equal(t, "tNvzjo7S", event.ID) 112 | assert.Equal(t, "event", event.Type) 113 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", event.Created) 114 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", event.LastModified) 115 | assert.Equal(t, "idle", event.Status) 116 | assert.Equal(t, "a_media_id", event.MediaID) 117 | assert.Equal(t, "available", event.MasterAccess.Status) 118 | assert.Equal(t, "2022-11-11T07:50:00+00:00", event.MasterAccess.Expiration) 119 | } 120 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jwplayer/jwplatform-go 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/Flaque/filet v0.0.0-20201012163910-45f684403088 7 | github.com/google/go-querystring v1.0.0 8 | github.com/mitchellh/go-homedir v1.1.0 9 | github.com/spf13/afero v1.5.1 // indirect 10 | github.com/stretchr/testify v1.6.1 11 | gopkg.in/h2non/gock.v1 v1.0.16 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Flaque/filet v0.0.0-20201012163910-45f684403088 h1:PnnQln5IGbhLeJOi6hVs+lCeF+B1dRfFKPGXUAez0Ww= 2 | github.com/Flaque/filet v0.0.0-20201012163910-45f684403088/go.mod h1:TK+jB3mBs+8ZMWhU5BqZKnZWJ1MrLo8etNVg51ueTBo= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 6 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 7 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= 8 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= 9 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 10 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 11 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 12 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= 13 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 14 | github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/spf13/afero v1.5.1 h1:VHu76Lk0LSP1x254maIu2bplkWpfBWI+B+6fdoZprcg= 18 | github.com/spf13/afero v1.5.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 21 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 22 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 24 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 25 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 26 | golang.org/x/net v0.0.0-20191021144547-ec77196f6094/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 27 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 28 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 29 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 30 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 31 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 32 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 33 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 34 | gopkg.in/h2non/gentleman.v1 v1.0.4/go.mod h1:JYuHVdFzS4MKOXe0o+chKJ4hCe6tqKKw9XH9YP6WFrg= 35 | gopkg.in/h2non/gock.v1 v1.0.16 h1:F11k+OafeuFENsjei5t2vMTSTs9L62AdyTe4E1cgdG8= 36 | gopkg.in/h2non/gock.v1 v1.0.16/go.mod h1:XVuDAssexPLwgxCLMvDTWNU5eqklsydR6I5phZ9oPB8= 37 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 38 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 39 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 40 | -------------------------------------------------------------------------------- /imports.go: -------------------------------------------------------------------------------- 1 | package jwplatform 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/google/go-querystring/query" 8 | ) 9 | 10 | // ImportResource is the resource that is returned for all Import resource requests, 11 | // with the exception of the Create action, which extends this struct with upload-related data. 12 | type ImportResource struct { 13 | V2ResourceResponse 14 | 15 | Metadata ImportReadMetadata `json:"metadata"` 16 | TotalItemsIngested int `json:"total_items_ingested"` 17 | LastImport string `json:"last_import"` 18 | } 19 | 20 | // ImportReadMetadata describes the read structure of an Import resource metadata. 21 | // This extends the base metadata, including an additional field, Password, which cannot be updated 22 | // on a write call (update/create) 23 | type ImportReadMetadata struct { 24 | ImportMetadata 25 | Password string `json:"password"` 26 | } 27 | 28 | // ImportMetadata describes the metadata for an Import resource 29 | type ImportMetadata struct { 30 | URL string `json:"url"` 31 | HostOnImport bool `json:"host_on_import"` 32 | Title string `json:"title,omitempty"` 33 | State string `json:"state"` 34 | Type string `json:"type"` 35 | Username string `json:"username,omitempty"` 36 | Tags []string `json:"tags"` 37 | IngestMetadata IngestMetadata `json:"ingest_metadata"` 38 | IngestTags []string `json:"ingest_tags"` 39 | } 40 | 41 | // ImportWriteRequest is the request structure required for Import create calls. 42 | type ImportWriteRequest struct { 43 | Metadata ImportMetadata `json:"metadata"` 44 | } 45 | 46 | // IngestMetadata describes which data will be captured in the import from the 47 | // MRSS feed. 48 | type IngestMetadata struct { 49 | Captions bool `json:"captions"` 50 | Categories bool `json:"categories"` 51 | Credits bool `json:"credits"` 52 | Description bool `json:"description"` 53 | Keywords bool `json:"keywords"` 54 | PublishDate bool `json:"publish_date"` 55 | Tags bool `json:"tags"` 56 | Thumbnails bool `json:"thumbnails"` 57 | } 58 | 59 | // ImportResourcesResponse is the response structure for Import list calls. 60 | type ImportResourcesResponse struct { 61 | V2ResourcesResponse 62 | Imports []ImportResource `json:"imports"` 63 | } 64 | 65 | // ImportsClient for interacting with V2 Imports API. 66 | type ImportsClient struct { 67 | v2Client *V2Client 68 | } 69 | 70 | // Get a single Import resource by ID. 71 | func (c *ImportsClient) Get(siteID, importID string) (*ImportResource, error) { 72 | importResource := &ImportResource{} 73 | path := fmt.Sprintf("/v2/sites/%s/imports/%s", siteID, importID) 74 | err := c.v2Client.Request(http.MethodGet, path, importResource, nil, nil) 75 | return importResource, err 76 | } 77 | 78 | // Create a Import resource. 79 | func (c *ImportsClient) Create(siteID string, importMetadata *ImportMetadata) (*ImportResource, error) { 80 | createRequestData := &ImportWriteRequest{Metadata: *importMetadata} 81 | importResource := &ImportResource{} 82 | path := fmt.Sprintf("/v2/sites/%s/imports", siteID) 83 | err := c.v2Client.Request(http.MethodPost, path, importResource, createRequestData, nil) 84 | return importResource, err 85 | } 86 | 87 | // List all Import resources associated with a given Site ID. 88 | func (c *ImportsClient) List(siteID string, queryParams *QueryParams) (*ImportResourcesResponse, error) { 89 | importResources := &ImportResourcesResponse{} 90 | path := fmt.Sprintf("/v2/sites/%s/imports", siteID) 91 | urlValues, _ := query.Values(queryParams) 92 | err := c.v2Client.Request(http.MethodGet, path, importResources, nil, urlValues) 93 | return importResources, err 94 | } 95 | 96 | // Update a Import resource by ID. 97 | func (c *ImportsClient) Update(siteID, importID string, importMetadata *ImportMetadata) (*ImportResource, error) { 98 | updateRequestData := &ImportWriteRequest{Metadata: *importMetadata} 99 | importResource := &ImportResource{} 100 | path := fmt.Sprintf("/v2/sites/%s/imports/%s", siteID, importID) 101 | err := c.v2Client.Request(http.MethodPatch, path, importResource, updateRequestData, nil) 102 | return importResource, err 103 | } 104 | 105 | // Delete a Import resource by ID. 106 | func (c *ImportsClient) Delete(siteID, importID string) error { 107 | path := fmt.Sprintf("/v2/sites/%s/imports/%s", siteID, importID) 108 | err := c.v2Client.Request(http.MethodDelete, path, nil, nil, nil) 109 | return err 110 | } 111 | -------------------------------------------------------------------------------- /imports_test.go: -------------------------------------------------------------------------------- 1 | package jwplatform 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "gopkg.in/h2non/gock.v1" 11 | ) 12 | 13 | func TestGetImport(t *testing.T) { 14 | defer gock.Off() 15 | 16 | siteID := "abcdefgh" 17 | importID := "mnbvcxkj" 18 | mockAuthToken := "shhh" 19 | 20 | requestPath := fmt.Sprintf("/v2/sites/%s/imports/%s", siteID, importID) 21 | mockImportResponse := map[string]string{"id": importID} 22 | 23 | gock.New("https://api.jwplayer.com"). 24 | Get(requestPath). 25 | MatchHeader("Authorization", "^Bearer .+"). 26 | MatchHeader("User-Agent", "^jwplatform-go/+"). 27 | Reply(200). 28 | JSON(mockImportResponse) 29 | 30 | testClient := New(mockAuthToken) 31 | channel, err := testClient.Imports.Get(siteID, importID) 32 | assert.Equal(t, importID, channel.ID) 33 | assert.Equal(t, nil, err) 34 | } 35 | 36 | func TestDeleteImport(t *testing.T) { 37 | defer gock.Off() 38 | 39 | siteID := "abcdefgh" 40 | importID := "mnbvcxkj" 41 | mockAuthToken := "shhh" 42 | 43 | requestPath := fmt.Sprintf("/v2/sites/%s/imports/%s", siteID, importID) 44 | 45 | gock.New("https://api.jwplayer.com"). 46 | Delete(requestPath). 47 | MatchHeader("Authorization", "^Bearer .+"). 48 | MatchHeader("User-Agent", "^jwplatform-go/+"). 49 | Reply(204) 50 | 51 | testClient := New(mockAuthToken) 52 | err := testClient.Imports.Delete(siteID, importID) 53 | assert.Equal(t, nil, err) 54 | } 55 | 56 | func TestCreateImport(t *testing.T) { 57 | defer gock.Off() 58 | 59 | siteID := "abcdefgh" 60 | importID := "mnbvcxkj" 61 | mockAuthToken := "shhh" 62 | 63 | requestPath := fmt.Sprintf("/v2/sites/%s/imports", siteID) 64 | mockImportResponse := map[string]string{"id": importID} 65 | 66 | gock.New("https://api.jwplayer.com"). 67 | Post(requestPath). 68 | MatchHeader("Authorization", "^Bearer .+"). 69 | MatchHeader("User-Agent", "^jwplatform-go/+"). 70 | Reply(201). 71 | JSON(mockImportResponse) 72 | 73 | testClient := New(mockAuthToken) 74 | newImport := &ImportMetadata{Title: "Another test video"} 75 | channel, err := testClient.Imports.Create(siteID, newImport) 76 | assert.Equal(t, importID, channel.ID) 77 | assert.Equal(t, nil, err) 78 | } 79 | 80 | func TestUpdateImport(t *testing.T) { 81 | defer gock.Off() 82 | 83 | siteID := "abcdefgh" 84 | importID := "mnbvcxkj" 85 | mockAuthToken := "shhh" 86 | 87 | requestPath := fmt.Sprintf("/v2/sites/%s/imports/%s", siteID, importID) 88 | mockImportResponse := map[string]string{"id": importID} 89 | 90 | gock.New("https://api.jwplayer.com"). 91 | Patch(requestPath). 92 | MatchHeader("Authorization", "^Bearer .+"). 93 | MatchHeader("User-Agent", "^jwplatform-go/+"). 94 | Reply(200). 95 | JSON(mockImportResponse) 96 | 97 | testClient := New(mockAuthToken) 98 | updateMetadata := &ImportMetadata{Title: "Updating another test media"} 99 | channel, err := testClient.Imports.Update(siteID, importID, updateMetadata) 100 | assert.Equal(t, importID, channel.ID) 101 | assert.Equal(t, nil, err) 102 | } 103 | 104 | func TestListImport(t *testing.T) { 105 | defer gock.Off() 106 | 107 | siteID := "abcdefgh" 108 | importID := "mnbvcxkj" 109 | mockAuthToken := "shhh" 110 | page := 2 111 | pageLength := 4 112 | 113 | requestPath := fmt.Sprintf("/v2/sites/%s/imports", siteID) 114 | mockImportsResponse := map[string]interface{}{ 115 | "page_length": pageLength, 116 | "page": page, 117 | "imports": []map[string]string{{"id": importID}}, 118 | } 119 | 120 | gock.New("https://api.jwplayer.com"). 121 | Get(requestPath). 122 | MatchHeader("Authorization", "^Bearer .+"). 123 | MatchHeader("User-Agent", "^jwplatform-go/+"). 124 | MatchParam("page", strconv.Itoa(page)). 125 | MatchParam("page_length", strconv.Itoa(pageLength)). 126 | Reply(200). 127 | JSON(mockImportsResponse) 128 | 129 | testClient := New(mockAuthToken) 130 | params := &QueryParams{PageLength: pageLength, Page: page} 131 | importsResponse, err := testClient.Imports.List(siteID, params) 132 | assert.Equal(t, page, importsResponse.Page) 133 | assert.Equal(t, pageLength, importsResponse.PageLength) 134 | assert.Equal(t, importID, importsResponse.Imports[0].ID) 135 | assert.Equal(t, nil, err) 136 | } 137 | 138 | func TestUnmarshalImport(t *testing.T) { 139 | importData := map[string]interface{}{ 140 | "id": "abZqokMz", 141 | "type": "import", 142 | "created": "2019-09-25T15:29:11.042095+00:00", 143 | "last_modified": "2019-09-25T15:29:11.042095+00:00", 144 | "total_items_ingested": 50, 145 | "last_import": "2019-09-25T15:29:11.042095+00:00", 146 | "metadata": map[string]interface{}{ 147 | "username": "username", 148 | "password": "password", 149 | "url": "http://url.com", 150 | "host_on_import": true, 151 | "title": "test title", 152 | "state": "ready", 153 | "type": "active", 154 | "tags": []string{"tag_a", "tag_b"}, 155 | "ingest_tags": []string{"ingest_tag_a", "ingest_tag_b"}, 156 | "ingest_metadata": map[string]bool{ 157 | "captions": true, 158 | "categories": false, 159 | "credits": false, 160 | "description": true, 161 | "keywords": false, 162 | "publish_date": true, 163 | "tags": true, 164 | "thumbnails": false, 165 | }, 166 | }, 167 | } 168 | 169 | bytes, err := json.Marshal(&importData) 170 | assert.NoError(t, err) 171 | 172 | var importResource ImportResource 173 | err = json.Unmarshal(bytes, &importResource) 174 | assert.NoError(t, err) 175 | 176 | assert.Equal(t, "abZqokMz", importResource.ID) 177 | assert.Equal(t, "import", importResource.Type) 178 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", importResource.Created) 179 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", importResource.LastModified) 180 | 181 | assert.Equal(t, "username", importResource.Metadata.Username) 182 | assert.Equal(t, "password", importResource.Metadata.Password) 183 | assert.Equal(t, "http://url.com", importResource.Metadata.URL) 184 | assert.Equal(t, true, importResource.Metadata.HostOnImport) 185 | assert.Equal(t, "ready", importResource.Metadata.State) 186 | assert.Equal(t, "active", importResource.Metadata.Type) 187 | assert.Equal(t, []string{"tag_a", "tag_b"}, importResource.Metadata.Tags) 188 | assert.Equal(t, []string{"ingest_tag_a", "ingest_tag_b"}, importResource.Metadata.IngestTags) 189 | 190 | assert.Equal(t, true, importResource.Metadata.IngestMetadata.Captions) 191 | assert.Equal(t, false, importResource.Metadata.IngestMetadata.Categories) 192 | assert.Equal(t, false, importResource.Metadata.IngestMetadata.Credits) 193 | assert.Equal(t, true, importResource.Metadata.IngestMetadata.Description) 194 | assert.Equal(t, false, importResource.Metadata.IngestMetadata.Keywords) 195 | assert.Equal(t, true, importResource.Metadata.IngestMetadata.PublishDate) 196 | assert.Equal(t, true, importResource.Metadata.IngestMetadata.Tags) 197 | assert.Equal(t, false, importResource.Metadata.IngestMetadata.Thumbnails) 198 | } 199 | -------------------------------------------------------------------------------- /jwplatform.go: -------------------------------------------------------------------------------- 1 | /* 2 | An API client for the V2 JW Platform. For the API documentation see: 3 | https://developer.jwplayer.com/jwplayer/reference#introduction-to-api-v2 4 | 5 | Sample Usage: 6 | 7 | import ( 8 | "github.com/jwplayer/jwplatform-go" 9 | "github.com/jwplayer/jwplatform-go/media" 10 | ) 11 | 12 | jwplatform := jwplatform.New("API_SECRET") 13 | siteID := "9kzNUpe4" 14 | mediaID := "LaJFzc9d" 15 | 16 | // Get a Resource 17 | media, err := jwplatform.Media.Get(siteID, mediaID) 18 | 19 | // Create a Resource 20 | mediaToCreate := &jwplatform.MediaMetadata(Title: "My new video") 21 | media, err := jwplatform.Media.Create(siteID, mediaToCreate) 22 | 23 | // List a Resource 24 | mediaResources, err := jwplatform.Media.List(siteID, nil) 25 | // Optionally include query parameters, including page, page length, sort, and filters. 26 | params := jwplatform.QueryParams{Page: 2, PageLength: 5} 27 | mediaResources, err := jwplatform.Media.List(siteID, params) 28 | 29 | // Update a Resource 30 | updateMetadata := &jwplatform.MediaMetadata{Title: "Updated video title"} 31 | updatedMedia, err := jwplatform.Media.Update(siteID, mediaID, updateMetadata) 32 | 33 | // Delete a Resource 34 | err := jwplatform.Media.Delete(siteID, mediaID) 35 | if err != nil { 36 | fmt.Println("Success") 37 | } 38 | */ 39 | 40 | package jwplatform 41 | 42 | // JWPlatform client for interacting with JW Player V2 Platform APIs. 43 | type JWPlatform struct { 44 | Version string 45 | Analytics *AnalyticsClient 46 | Channels *ChannelsClient 47 | DRMPolicies *DRMPoliciesClient 48 | Imports *ImportsClient 49 | Media *MediaClient 50 | PlayerBidding *PlayerBiddingClient 51 | Webhooks *WebhooksClient 52 | } 53 | 54 | // New generates an authenticated client for interacting with JW Player V2 Platform APIs. 55 | func New(apiSecret string) *JWPlatform { 56 | v2Client := NewV2Client(apiSecret) 57 | channelsClient := NewChannelsClient(v2Client) 58 | return &JWPlatform{ 59 | Version: version, 60 | Analytics: &AnalyticsClient{v2Client: v2Client}, 61 | Channels: channelsClient, 62 | DRMPolicies: &DRMPoliciesClient{v2Client: v2Client}, 63 | Imports: &ImportsClient{v2Client: v2Client}, 64 | Media: &MediaClient{v2Client: v2Client}, 65 | PlayerBidding: &PlayerBiddingClient{v2Client: v2Client}, 66 | Webhooks: &WebhooksClient{v2Client: v2Client}, 67 | } 68 | } 69 | 70 | // Private consts 71 | const ( 72 | version = "1.0.0" 73 | apiHost = "api.jwplayer.com" 74 | apiVersion = "v2" 75 | ) 76 | -------------------------------------------------------------------------------- /media.go: -------------------------------------------------------------------------------- 1 | package jwplatform 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/google/go-querystring/query" 8 | ) 9 | 10 | // MediaResource is the resource that is returned for all Media resource requests, 11 | // with the exception of the Create action, which extends this struct with upload-related data. 12 | type MediaResource struct { 13 | V2ResourceResponse 14 | 15 | Duration float64 `json:"duration"` 16 | ExternalID string `json:"external_id"` 17 | TrimInPoint string `json:"trim_in_point"` 18 | TrimOutPoint string `json:"trim_out_point"` 19 | Status string `json:"status"` 20 | ErrorMessage string `json:"error_message"` 21 | MimeType string `json:"mime_type"` 22 | MediaType string `json:"media_type"` 23 | HostingType string `json:"hosting_type"` 24 | SourceURL string `json:"source_url"` 25 | 26 | Metadata MediaMetadata `json:"metadata"` 27 | } 28 | 29 | // CreateMediaResponse is the response structure for Media create calls. 30 | // If "direct" or "multipart" were selected as the upload method, the response includes additional data required to complete your upload. 31 | // 32 | // For direct uploads, the UploadLink will return a pre-signed S3 Upload Link. 33 | // 34 | // For multipart uploads, the UploadToken and UploadID will be returned, to be used 35 | // in subsequent requests to the V2 Upload API. 36 | type CreateMediaResponse struct { 37 | V2ResourceResponse 38 | MediaResource 39 | UploadLink string `json:"upload_link,omitempty"` 40 | UploadToken string `json:"upload_token,omitempty"` 41 | UploadID string `json:"upload_id,omitempty"` 42 | } 43 | 44 | // CreateMediaRequest is the request structure required for Media create calls. 45 | // By default, the 'direct' upload method is used. 46 | type CreateMediaRequest struct { 47 | Metadata MediaMetadata `json:"metadata,omitempty"` 48 | Upload Upload `json:"upload,omitempty"` 49 | } 50 | 51 | // ReuploadRequest is the request structure required for Media reupload calls. 52 | type ReuploadRequest struct { 53 | Upload Upload `json:"upload"` 54 | } 55 | 56 | // UpdateMediaRequest is the request structure required for Media update calls. 57 | type UpdateMediaRequest struct { 58 | Metadata MediaMetadata `json:"metadata"` 59 | } 60 | 61 | // Upload contains the data used to describe the upload. 62 | // Available upload method's include "direct" (default), "multipart", "external", and "fetch". 63 | // 64 | // Direct uploads can be used for assets up to 5GB. 65 | // 66 | // MimeType and SourceURL are required only for "direct" and "fetch", respectively. 67 | // 68 | // TrimInPoint and TrimOutPoint cannot be specified for "external". 69 | type Upload struct { 70 | Method string `json:"method,omitempty"` 71 | MimeType string `json:"mime_type,omitempty"` 72 | SourceURL string `json:"source_url,omitempty"` 73 | TrimInPoint string `json:"trim_in_point,omitempty"` 74 | TrimOutPoint string `json:"trim_out_point,omitempty"` 75 | } 76 | 77 | // MediaMetadata describes a Media resource 78 | type MediaMetadata struct { 79 | Title string `json:"title,omitempty"` 80 | Description string `json:"description,omitempty"` 81 | Author string `json:"author,omitempty"` 82 | Permalink string `json:"permalink,omitempty"` 83 | Category string `json:"category,omitempty"` 84 | PublishStartDate string `json:"publish_start_date,omitempty"` 85 | PublishEndDate string `json:"publish_end_date,omitempty"` 86 | Tags []string `json:"tags,omitempty"` 87 | CustomParams map[string]string `json:"custom_params,omitempty"` 88 | ExternalID string `json:"external_id,omitempty"` 89 | } 90 | 91 | // MediaResourcesResponse is the response structure for Media list calls. 92 | type MediaResourcesResponse struct { 93 | V2ResourcesResponse 94 | Media []MediaResource `json:"media"` 95 | } 96 | 97 | // MediaClient for interacting with V2 Media API. 98 | type MediaClient struct { 99 | v2Client *V2Client 100 | } 101 | 102 | // Get a single Media resource by ID. 103 | func (c *MediaClient) Get(siteID, mediaID string) (*MediaResource, error) { 104 | media := &MediaResource{} 105 | path := fmt.Sprintf("/v2/sites/%s/media/%s", siteID, mediaID) 106 | err := c.v2Client.Request(http.MethodGet, path, media, nil, nil) 107 | return media, err 108 | } 109 | 110 | // Create a Media resource. 111 | func (c *MediaClient) Create(siteID string, mediaMetadata *MediaMetadata) (*CreateMediaResponse, error) { 112 | createRequestData := &CreateMediaRequest{Metadata: *mediaMetadata} 113 | media := &CreateMediaResponse{} 114 | path := fmt.Sprintf("/v2/sites/%s/media", siteID) 115 | err := c.v2Client.Request(http.MethodPost, path, media, createRequestData, nil) 116 | return media, err 117 | } 118 | 119 | // List all Media resources associated with a given Site ID. 120 | func (c *MediaClient) List(siteID string, queryParams *QueryParams) (*MediaResourcesResponse, error) { 121 | media := &MediaResourcesResponse{} 122 | path := fmt.Sprintf("/v2/sites/%s/media", siteID) 123 | urlValues, _ := query.Values(queryParams) 124 | err := c.v2Client.Request(http.MethodGet, path, media, nil, urlValues) 125 | return media, err 126 | } 127 | 128 | // Update a Media resource by ID. 129 | func (c *MediaClient) Update(siteID, mediaID string, mediaMetadata *MediaMetadata) (*MediaResource, error) { 130 | updateRequestData := &UpdateMediaRequest{Metadata: *mediaMetadata} 131 | media := &MediaResource{} 132 | path := fmt.Sprintf("/v2/sites/%s/media/%s", siteID, mediaID) 133 | err := c.v2Client.Request(http.MethodPatch, path, media, updateRequestData, nil) 134 | return media, err 135 | } 136 | 137 | // Delete a Media resource by ID. 138 | func (c *MediaClient) Delete(siteID, mediaID string) error { 139 | path := fmt.Sprintf("/v2/sites/%s/media/%s", siteID, mediaID) 140 | err := c.v2Client.Request(http.MethodDelete, path, nil, nil, nil) 141 | return err 142 | } 143 | 144 | // Reupload a Media resource by ID. 145 | func (c *MediaClient) Reupload(siteID, mediaID string, upload *Upload) (*CreateMediaResponse, error) { 146 | reuploadRequest := &ReuploadRequest{Upload: *upload} 147 | media := &CreateMediaResponse{} 148 | path := fmt.Sprintf("/v2/sites/%s/media/%s/reupload", siteID, mediaID) 149 | err := c.v2Client.Request(http.MethodPost, path, media, reuploadRequest, nil) 150 | return media, err 151 | } 152 | -------------------------------------------------------------------------------- /media_test.go: -------------------------------------------------------------------------------- 1 | package jwplatform 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "gopkg.in/h2non/gock.v1" 11 | ) 12 | 13 | func TestGetMedia(t *testing.T) { 14 | defer gock.Off() 15 | 16 | siteID := "abcdefgh" 17 | mediaID := "mnbvcxkj" 18 | mockAuthToken := "shhh" 19 | 20 | requestPath := fmt.Sprintf("/v2/sites/%s/media/%s", siteID, mediaID) 21 | mockMediaResponse := map[string]string{"id": mediaID} 22 | 23 | gock.New("https://api.jwplayer.com"). 24 | Get(requestPath). 25 | MatchHeader("Authorization", "^Bearer .+"). 26 | MatchHeader("User-Agent", "^jwplatform-go/+"). 27 | Reply(200). 28 | JSON(mockMediaResponse) 29 | 30 | testClient := New(mockAuthToken) 31 | media, err := testClient.Media.Get(siteID, mediaID) 32 | assert.Equal(t, mediaID, media.ID) 33 | assert.Equal(t, nil, err) 34 | } 35 | 36 | func TestDeleteMedia(t *testing.T) { 37 | defer gock.Off() 38 | 39 | siteID := "abcdefgh" 40 | mediaID := "mnbvcxkj" 41 | mockAuthToken := "shhh" 42 | 43 | requestPath := fmt.Sprintf("/v2/sites/%s/media/%s", siteID, mediaID) 44 | 45 | gock.New("https://api.jwplayer.com"). 46 | Delete(requestPath). 47 | MatchHeader("Authorization", "^Bearer .+"). 48 | MatchHeader("User-Agent", "^jwplatform-go/+"). 49 | Reply(204) 50 | 51 | testClient := New(mockAuthToken) 52 | err := testClient.Media.Delete(siteID, mediaID) 53 | assert.Equal(t, nil, err) 54 | } 55 | 56 | func TestCreateMedia(t *testing.T) { 57 | defer gock.Off() 58 | 59 | siteID := "abcdefgh" 60 | mediaID := "mnbvcxkj" 61 | mockAuthToken := "shhh" 62 | 63 | requestPath := fmt.Sprintf("/v2/sites/%s/media", siteID) 64 | mockMediaResponse := map[string]string{"id": mediaID} 65 | 66 | gock.New("https://api.jwplayer.com"). 67 | Post(requestPath). 68 | MatchHeader("Authorization", "^Bearer .+"). 69 | MatchHeader("User-Agent", "^jwplatform-go/+"). 70 | Reply(201). 71 | JSON(mockMediaResponse) 72 | 73 | testClient := New(mockAuthToken) 74 | newMedia := &MediaMetadata{Title: "Another test video"} 75 | media, err := testClient.Media.Create(siteID, newMedia) 76 | assert.Equal(t, mediaID, media.ID) 77 | assert.Equal(t, nil, err) 78 | } 79 | 80 | func TestUpdateMedia(t *testing.T) { 81 | defer gock.Off() 82 | 83 | siteID := "abcdefgh" 84 | mediaID := "mnbvcxkj" 85 | mockAuthToken := "shhh" 86 | 87 | requestPath := fmt.Sprintf("/v2/sites/%s/media/%s", siteID, mediaID) 88 | mockMediaResponse := map[string]string{"id": mediaID} 89 | 90 | gock.New("https://api.jwplayer.com"). 91 | Patch(requestPath). 92 | MatchHeader("Authorization", "^Bearer .+"). 93 | MatchHeader("User-Agent", "^jwplatform-go/+"). 94 | Reply(200). 95 | JSON(mockMediaResponse) 96 | 97 | testClient := New(mockAuthToken) 98 | updateMetadata := &MediaMetadata{Title: "Updating another test media"} 99 | media, err := testClient.Media.Update(siteID, mediaID, updateMetadata) 100 | assert.Equal(t, mediaID, media.ID) 101 | assert.Equal(t, nil, err) 102 | } 103 | 104 | func TestListMedia(t *testing.T) { 105 | defer gock.Off() 106 | 107 | siteID := "abcdefgh" 108 | mediaID := "mnbvcxkj" 109 | mockAuthToken := "shhh" 110 | page := 2 111 | pageLength := 4 112 | 113 | requestPath := fmt.Sprintf("/v2/sites/%s/media", siteID) 114 | mockMediaResponse := map[string]interface{}{ 115 | "page_length": pageLength, 116 | "page": page, 117 | "media": []map[string]string{{"id": mediaID}}, 118 | } 119 | 120 | gock.New("https://api.jwplayer.com"). 121 | Get(requestPath). 122 | MatchHeader("Authorization", "^Bearer .+"). 123 | MatchHeader("User-Agent", "^jwplatform-go/+"). 124 | MatchParam("page", strconv.Itoa(page)). 125 | MatchParam("page_length", strconv.Itoa(pageLength)). 126 | Reply(200). 127 | JSON(mockMediaResponse) 128 | 129 | testClient := New(mockAuthToken) 130 | params := &QueryParams{PageLength: pageLength, Page: page} 131 | mediaMediaResponse, err := testClient.Media.List(siteID, params) 132 | assert.Equal(t, page, mediaMediaResponse.Page) 133 | assert.Equal(t, pageLength, mediaMediaResponse.PageLength) 134 | assert.Equal(t, mediaID, mediaMediaResponse.Media[0].ID) 135 | assert.Equal(t, nil, err) 136 | } 137 | 138 | func TestReuploadMedia(t *testing.T) { 139 | defer gock.Off() 140 | 141 | siteID := "abcdefgh" 142 | mediaID := "mnbvcxkj" 143 | mockAuthToken := "shhh" 144 | 145 | requestPath := fmt.Sprintf("/v2/sites/%s/media/%s/reupload", siteID, mediaID) 146 | mockMediaResponse := map[string]string{"id": mediaID} 147 | 148 | gock.New("https://api.jwplayer.com"). 149 | Post(requestPath). 150 | MatchHeader("Authorization", "^Bearer .+"). 151 | MatchHeader("User-Agent", "^jwplatform-go/+"). 152 | Reply(200). 153 | JSON(mockMediaResponse) 154 | 155 | testClient := New(mockAuthToken) 156 | upload := Upload{} 157 | media, _ := testClient.Media.Reupload(siteID, mediaID, &upload) 158 | assert.Equal(t, mediaID, media.ID) 159 | } 160 | 161 | func TestUnmarshalMedia(t *testing.T) { 162 | mediaData := map[string]interface{}{ 163 | "id": "abZqokMz", 164 | "type": "media", 165 | "created": "2019-09-25T15:29:11.042095+00:00", 166 | "last_modified": "2019-09-25T15:29:11.042095+00:00", 167 | "error_message": "", 168 | "status": "ready", 169 | "external_id": "abc", 170 | "mime_type": "video/mp4", 171 | "duration": 360.00, 172 | "media_type": "video", 173 | "hosting_type": "hosted", 174 | "trim_in_point": "00:00:33", 175 | "trim_out_point": "00:01:32.121", 176 | "source_url": "http://media.com", 177 | "metadata": map[string]interface{}{ 178 | "title": "media", 179 | "description": "Describes a media", 180 | "author": "Steven Spielberg", 181 | "permalink": "permalink.com", 182 | "category": "Food", 183 | "publish_start_date": "2019-09-25T15:29:11.042095+00:00", 184 | "publish_end_date": "2019-09-25T15:29:11.042095+00:00", 185 | "tags": []string{"tag_a", "tag_b"}, 186 | "custom_params": map[string]string{ 187 | "key": "value", 188 | }, 189 | }, 190 | } 191 | 192 | bytes, err := json.Marshal(&mediaData) 193 | assert.NoError(t, err) 194 | 195 | var media MediaResource 196 | err = json.Unmarshal(bytes, &media) 197 | assert.NoError(t, err) 198 | 199 | assert.Equal(t, "abZqokMz", media.ID) 200 | assert.Equal(t, "media", media.Type) 201 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", media.Created) 202 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", media.LastModified) 203 | assert.Equal(t, "ready", media.Status) 204 | assert.Equal(t, "", media.ErrorMessage) 205 | assert.Equal(t, "abc", media.ExternalID) 206 | assert.Equal(t, "video/mp4", media.MimeType) 207 | assert.Equal(t, 360.00, media.Duration) 208 | assert.Equal(t, "video", media.MediaType) 209 | assert.Equal(t, "http://media.com", media.SourceURL) 210 | assert.Equal(t, "hosted", media.HostingType) 211 | assert.Equal(t, "00:00:33", media.TrimInPoint) 212 | assert.Equal(t, "00:01:32.121", media.TrimOutPoint) 213 | 214 | assert.Equal(t, "media", media.Metadata.Title) 215 | assert.Equal(t, "Describes a media", media.Metadata.Description) 216 | assert.Equal(t, "Steven Spielberg", media.Metadata.Author) 217 | assert.Equal(t, "permalink.com", media.Metadata.Permalink) 218 | assert.Equal(t, "Food", media.Metadata.Category) 219 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", media.Metadata.PublishStartDate) 220 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", media.Metadata.PublishEndDate) 221 | assert.Equal(t, []string{"tag_a", "tag_b"}, media.Metadata.Tags) 222 | assert.Equal(t, map[string]string{"key": "value"}, media.Metadata.CustomParams) 223 | } 224 | -------------------------------------------------------------------------------- /player_bidding.go: -------------------------------------------------------------------------------- 1 | package jwplatform 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/google/go-querystring/query" 8 | ) 9 | 10 | // PlayerBiddingConfigurationResource is the resource that is returned for all Player Bidding Configuration resource requests, 11 | // with the exception of the Create action, which extends this struct with upload-related data. 12 | type PlayerBiddingConfigurationResource struct { 13 | V2ResourceResponse 14 | Metadata PlayerBiddingConfigurationMetadata `json:"metadata"` 15 | } 16 | 17 | // PlayerBiddingConfigurationMetadata describes the metadata for an Player Bidding Configuration resource 18 | type PlayerBiddingConfigurationMetadata struct { 19 | Bids BidsMetadata `json:"bids"` 20 | } 21 | 22 | // BidsMetadata represents the player bidding configuration as used by the JW Player 23 | type BidsMetadata struct { 24 | Settings BidSettingsMetadata `json:"settings"` 25 | Bidders []BiddersMetadata `json:"bidders"` 26 | } 27 | 28 | // BidSettingsMetadata represents the configuration for the player bidding plugin 29 | type BidSettingsMetadata struct { 30 | BidTimeout int `json:"bidTimeout"` 31 | FloorPriceCents int `json:"floorPriceCents"` 32 | MediationLayerAdServer string `json:"mediationLayerAdServer"` 33 | Buckets []Bucket `json:"buckets"` 34 | } 35 | 36 | // Bucket represents a minimum, maximum value to which to apply an increment 37 | type Bucket struct { 38 | Min float64 `json:"min"` 39 | Max float64 `json:"max"` 40 | Increment float64 `json:"increment"` 41 | } 42 | 43 | // BiddersMetadata describes a configured Player Bidding bidder 44 | type BiddersMetadata struct { 45 | Name string `json:"name"` 46 | ID string `json:"id"` 47 | PubID string `json:"pubid"` 48 | CustomParams map[string]string `json:"custom_params"` 49 | } 50 | 51 | // PlayerBiddingWriteRequest is the request structure required for Player Bidding Configuration create and update calls. 52 | type PlayerBiddingWriteRequest struct { 53 | Metadata PlayerBiddingConfigurationMetadata `json:"metadata"` 54 | } 55 | 56 | // PlayerBiddingConfigurationResourcesResponse is the response structure for Player Bidding Configuration list calls. 57 | type PlayerBiddingConfigurationResourcesResponse struct { 58 | V2ResourcesResponse 59 | PlayerBiddingConfigs []PlayerBiddingConfigurationResource `json:"vpb_configs"` 60 | } 61 | 62 | // PlayerBiddingClient for interacting with V2 Player Bidding Configurations API. 63 | type PlayerBiddingClient struct { 64 | v2Client *V2Client 65 | } 66 | 67 | // Get a single Player Bidding Configuration resource by ID. 68 | func (c *PlayerBiddingClient) Get(siteID, importID string) (*PlayerBiddingConfigurationResource, error) { 69 | playerBiddingConfig := &PlayerBiddingConfigurationResource{} 70 | path := fmt.Sprintf("/v2/sites/%s/vpb_configs/%s", siteID, importID) 71 | err := c.v2Client.Request(http.MethodGet, path, playerBiddingConfig, nil, nil) 72 | return playerBiddingConfig, err 73 | } 74 | 75 | // Create a Player Bidding Configuration resource. 76 | func (c *PlayerBiddingClient) Create(siteID string, PlayerBiddingConfigurationMetadata *PlayerBiddingConfigurationMetadata) (*PlayerBiddingConfigurationResource, error) { 77 | createRequestData := &PlayerBiddingWriteRequest{Metadata: *PlayerBiddingConfigurationMetadata} 78 | playerBiddingConfig := &PlayerBiddingConfigurationResource{} 79 | path := fmt.Sprintf("/v2/sites/%s/vpb_configs", siteID) 80 | err := c.v2Client.Request(http.MethodPost, path, playerBiddingConfig, createRequestData, nil) 81 | return playerBiddingConfig, err 82 | } 83 | 84 | // List all Player Bidding Configuration resources associated with a given Site ID. 85 | func (c *PlayerBiddingClient) List(siteID string, queryParams *QueryParams) (*PlayerBiddingConfigurationResourcesResponse, error) { 86 | playerBiddingConfigs := &PlayerBiddingConfigurationResourcesResponse{} 87 | path := fmt.Sprintf("/v2/sites/%s/vpb_configs", siteID) 88 | urlValues, _ := query.Values(queryParams) 89 | err := c.v2Client.Request(http.MethodGet, path, playerBiddingConfigs, nil, urlValues) 90 | return playerBiddingConfigs, err 91 | } 92 | 93 | // Update a Player Bidding Configuration resource by ID. 94 | func (c *PlayerBiddingClient) Update(siteID, importID string, PlayerBiddingConfigurationMetadata *PlayerBiddingConfigurationMetadata) (*PlayerBiddingConfigurationResource, error) { 95 | updateRequestData := &PlayerBiddingWriteRequest{Metadata: *PlayerBiddingConfigurationMetadata} 96 | playerBiddingConfig := &PlayerBiddingConfigurationResource{} 97 | path := fmt.Sprintf("/v2/sites/%s/vpb_configs/%s", siteID, importID) 98 | err := c.v2Client.Request(http.MethodPatch, path, playerBiddingConfig, updateRequestData, nil) 99 | return playerBiddingConfig, err 100 | } 101 | 102 | // Delete a Player Bidding Configuration resource by ID. 103 | func (c *PlayerBiddingClient) Delete(siteID, importID string) error { 104 | path := fmt.Sprintf("/v2/sites/%s/vpb_configs/%s", siteID, importID) 105 | err := c.v2Client.Request(http.MethodDelete, path, nil, nil, nil) 106 | return err 107 | } 108 | -------------------------------------------------------------------------------- /player_bidding_test.go: -------------------------------------------------------------------------------- 1 | package jwplatform 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "gopkg.in/h2non/gock.v1" 11 | ) 12 | 13 | func TestGetPlayerBidding(t *testing.T) { 14 | defer gock.Off() 15 | 16 | siteID := "abcdefgh" 17 | playerBiddingConfigID := "mnbvcxkj" 18 | mockAuthToken := "shhh" 19 | 20 | requestPath := fmt.Sprintf("/v2/sites/%s/vpb_configs/%s", siteID, playerBiddingConfigID) 21 | mockPlayerBiddingResponse := map[string]string{"id": playerBiddingConfigID} 22 | 23 | gock.New("https://api.jwplayer.com"). 24 | Get(requestPath). 25 | MatchHeader("Authorization", "^Bearer .+"). 26 | MatchHeader("User-Agent", "^jwplatform-go/+"). 27 | Reply(200). 28 | JSON(mockPlayerBiddingResponse) 29 | 30 | testClient := New(mockAuthToken) 31 | playerBiddingConfig, err := testClient.PlayerBidding.Get(siteID, playerBiddingConfigID) 32 | assert.Equal(t, playerBiddingConfigID, playerBiddingConfig.ID) 33 | assert.Equal(t, nil, err) 34 | } 35 | 36 | func TestDeletePlayerBidding(t *testing.T) { 37 | defer gock.Off() 38 | 39 | siteID := "abcdefgh" 40 | playerBiddingConfigID := "mnbvcxkj" 41 | mockAuthToken := "shhh" 42 | 43 | requestPath := fmt.Sprintf("/v2/sites/%s/vpb_configs/%s", siteID, playerBiddingConfigID) 44 | 45 | gock.New("https://api.jwplayer.com"). 46 | Delete(requestPath). 47 | MatchHeader("Authorization", "^Bearer .+"). 48 | MatchHeader("User-Agent", "^jwplatform-go/+"). 49 | Reply(204) 50 | 51 | testClient := New(mockAuthToken) 52 | err := testClient.PlayerBidding.Delete(siteID, playerBiddingConfigID) 53 | assert.Equal(t, nil, err) 54 | } 55 | 56 | func TestCreatePlayerBidding(t *testing.T) { 57 | defer gock.Off() 58 | 59 | siteID := "abcdefgh" 60 | playerBiddingConfigID := "mnbvcxkj" 61 | mockAuthToken := "shhh" 62 | 63 | requestPath := fmt.Sprintf("/v2/sites/%s/vpb_configs", siteID) 64 | mockPlayerBiddingResponse := map[string]string{"id": playerBiddingConfigID} 65 | 66 | gock.New("https://api.jwplayer.com"). 67 | Post(requestPath). 68 | MatchHeader("Authorization", "^Bearer .+"). 69 | MatchHeader("User-Agent", "^jwplatform-go/+"). 70 | Reply(201). 71 | JSON(mockPlayerBiddingResponse) 72 | 73 | testClient := New(mockAuthToken) 74 | newPlayerBidding := &PlayerBiddingConfigurationMetadata{} 75 | playerBiddingConfig, err := testClient.PlayerBidding.Create(siteID, newPlayerBidding) 76 | assert.Equal(t, playerBiddingConfigID, playerBiddingConfig.ID) 77 | assert.Equal(t, nil, err) 78 | } 79 | 80 | func TestUpdatePlayerBidding(t *testing.T) { 81 | defer gock.Off() 82 | 83 | siteID := "abcdefgh" 84 | playerBiddingConfigID := "mnbvcxkj" 85 | mockAuthToken := "shhh" 86 | 87 | requestPath := fmt.Sprintf("/v2/sites/%s/vpb_configs/%s", siteID, playerBiddingConfigID) 88 | mockPlayerBiddingResponse := map[string]string{"id": playerBiddingConfigID} 89 | 90 | gock.New("https://api.jwplayer.com"). 91 | Patch(requestPath). 92 | MatchHeader("Authorization", "^Bearer .+"). 93 | MatchHeader("User-Agent", "^jwplatform-go/+"). 94 | Reply(200). 95 | JSON(mockPlayerBiddingResponse) 96 | 97 | testClient := New(mockAuthToken) 98 | updateMetadata := &PlayerBiddingConfigurationMetadata{} 99 | playerBiddingConfig, err := testClient.PlayerBidding.Update(siteID, playerBiddingConfigID, updateMetadata) 100 | assert.Equal(t, playerBiddingConfigID, playerBiddingConfig.ID) 101 | assert.Equal(t, nil, err) 102 | } 103 | 104 | func TestListPlayerBidding(t *testing.T) { 105 | defer gock.Off() 106 | 107 | siteID := "abcdefgh" 108 | playerBiddingConfigID := "aamkdcaz" 109 | mockAuthToken := "shhh" 110 | page := 2 111 | pageLength := 4 112 | 113 | requestPath := fmt.Sprintf("/v2/sites/%s/vpb_configs", siteID) 114 | mockPlayerBiddingResponse := map[string]interface{}{ 115 | "page_length": pageLength, 116 | "page": page, 117 | "vpb_configs": []map[string]string{{"id": playerBiddingConfigID}}, 118 | } 119 | 120 | gock.New("https://api.jwplayer.com"). 121 | Get(requestPath). 122 | MatchHeader("Authorization", "^Bearer .+"). 123 | MatchHeader("User-Agent", "^jwplatform-go/+"). 124 | MatchParam("page", strconv.Itoa(page)). 125 | MatchParam("page_length", strconv.Itoa(pageLength)). 126 | Reply(200). 127 | JSON(mockPlayerBiddingResponse) 128 | 129 | testClient := New(mockAuthToken) 130 | params := &QueryParams{PageLength: pageLength, Page: page} 131 | playerBiddingConfigsResp, err := testClient.PlayerBidding.List(siteID, params) 132 | assert.Equal(t, page, playerBiddingConfigsResp.Page) 133 | assert.Equal(t, pageLength, playerBiddingConfigsResp.PageLength) 134 | assert.Equal(t, playerBiddingConfigID, playerBiddingConfigsResp.PlayerBiddingConfigs[0].ID) 135 | assert.Equal(t, nil, err) 136 | } 137 | 138 | func TestUnmarshalPlayerBiddingConfig(t *testing.T) { 139 | playerBiddingConfigData := map[string]interface{}{ 140 | "id": "abZqokMz", 141 | "type": "vpb_config", 142 | "created": "2019-09-25T15:29:11.042095+00:00", 143 | "last_modified": "2019-09-25T15:29:11.042095+00:00", 144 | "metadata": map[string]interface{}{ 145 | "bids": map[string]interface{}{ 146 | "settings": map[string]interface{}{ 147 | "bidTimeout": 1, 148 | "floorPriceCents": 50, 149 | "mediationLayerAdServer": "dfp", 150 | "buckets": []interface{}{ 151 | map[string]float64{ 152 | "min": 5.00, 153 | "max": 5.50, 154 | "increment": 0.10, 155 | }, 156 | }, 157 | }, 158 | "bidders": []interface{}{ 159 | map[string]interface{}{ 160 | "name": "name", 161 | "id": "id_a", 162 | "pubid": "pubid_b", 163 | "custom_params": map[string]string{ 164 | "key": "value", 165 | }, 166 | }, 167 | }, 168 | }, 169 | }, 170 | } 171 | 172 | bytes, err := json.Marshal(&playerBiddingConfigData) 173 | assert.NoError(t, err) 174 | 175 | var playerBiddingConfig PlayerBiddingConfigurationResource 176 | err = json.Unmarshal(bytes, &playerBiddingConfig) 177 | assert.NoError(t, err) 178 | 179 | assert.Equal(t, "abZqokMz", playerBiddingConfig.ID) 180 | assert.Equal(t, "vpb_config", playerBiddingConfig.Type) 181 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", playerBiddingConfig.Created) 182 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", playerBiddingConfig.LastModified) 183 | 184 | assert.Equal(t, 1, playerBiddingConfig.Metadata.Bids.Settings.BidTimeout) 185 | assert.Equal(t, 50, playerBiddingConfig.Metadata.Bids.Settings.FloorPriceCents) 186 | assert.Equal(t, "dfp", playerBiddingConfig.Metadata.Bids.Settings.MediationLayerAdServer) 187 | 188 | assert.Equal(t, 5.00, playerBiddingConfig.Metadata.Bids.Settings.Buckets[0].Min) 189 | assert.Equal(t, 5.50, playerBiddingConfig.Metadata.Bids.Settings.Buckets[0].Max) 190 | assert.Equal(t, 0.10, playerBiddingConfig.Metadata.Bids.Settings.Buckets[0].Increment) 191 | 192 | assert.Equal(t, "name", playerBiddingConfig.Metadata.Bids.Bidders[0].Name) 193 | assert.Equal(t, "id_a", playerBiddingConfig.Metadata.Bids.Bidders[0].ID) 194 | assert.Equal(t, "pubid_b", playerBiddingConfig.Metadata.Bids.Bidders[0].PubID) 195 | assert.Equal(t, map[string]string{"key": "value"}, playerBiddingConfig.Metadata.Bids.Bidders[0].CustomParams) 196 | } 197 | -------------------------------------------------------------------------------- /v1/README.md: -------------------------------------------------------------------------------- 1 | ## Important 2 | 3 | This package is deprecated; no new functionality or fixes will be made to the V1 Client. Moving forward, we strongly recommend that 4 | you utilize the primary `jwplatform` client. 5 | 6 | ## Usage 7 | 8 | ```go 9 | import ( 10 | "github.com/jwplayer/jwplatform-go" 11 | ) 12 | 13 | client := jwplatform.NewClient("API_KEY", "API_SECRET") 14 | ``` 15 | 16 | ### Example: Get video metadata 17 | 18 | ```go 19 | package main 20 | 21 | import ( 22 | "context" 23 | "fmt" 24 | "log" 25 | "net/http" 26 | "net/url" 27 | "os" 28 | 29 | "github.com/jwplayer/jwplatform-go" 30 | ) 31 | 32 | func main() { 33 | ctx, cancel := context.WithCancel(context.Background()) 34 | defer cancel() 35 | 36 | apiKey := os.Getenv("JWPLATFORM_API_KEY") 37 | apiSecret := os.Getenv("JWPLATFORM_API_SECRET") 38 | 39 | client := jwplatform.NewClient(apiKey, apiSecret) 40 | 41 | // set URL params 42 | params := url.Values{} 43 | params.Set("video_key", "VIDEO_KEY") // some video key, e.g. gIRtMhYM 44 | 45 | // declare an empty interface 46 | var result map[string]interface{} 47 | 48 | err := client.MakeRequest(ctx, http.MethodGet, "/videos/show/", params, &result) 49 | 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | fmt.Println(result["status"]) // ok 55 | } 56 | ``` 57 | 58 | ### Example: Upload video 59 | 60 | ```go 61 | package main 62 | 63 | import ( 64 | "context" 65 | "fmt" 66 | "log" 67 | "net/url" 68 | "os" 69 | 70 | "github.com/jwplayer/jwplatform-go" 71 | ) 72 | 73 | func main() { 74 | filepath := "path/to/your/video.mp4" 75 | 76 | ctx, cancel := context.WithCancel(context.Background()) 77 | defer cancel() 78 | 79 | // set URL params 80 | params := url.Values{} 81 | params.Set("title", "Your video title") 82 | params.Set("description", "Your video description") 83 | 84 | apiKey := os.Getenv("JWPLATFORM_API_KEY") 85 | apiSecret := os.Getenv("JWPLATFORM_API_SECRET") 86 | 87 | client := jwplatform.NewClient(apiKey, apiSecret) 88 | 89 | // declare an empty interface 90 | var result map[string]interface{} 91 | 92 | // upload video using direct upload method 93 | err := client.Upload(ctx, filepath, params, &result) 94 | 95 | if err != nil { 96 | log.Fatal(err) 97 | } 98 | 99 | fmt.Println(result["status"]) // ok 100 | } 101 | ``` 102 | 103 | ## Supported operations 104 | 105 | All API methods documentated on the API are available in this client. Please refer to our [api documentation](https://developer.jwplayer.com/jwplayer/reference). 106 | 107 | ## Test 108 | 109 | Before running the tests, make sure to grab all of the package's dependencies: 110 | 111 | go get -t -v 112 | 113 | Run all tests: 114 | 115 | make test 116 | 117 | For any requests, bug or comments, please [open an issue][issues] or [submit a 118 | pull request][pulls]. 119 | -------------------------------------------------------------------------------- /v1/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jwplayer/jwplatform-go/v1 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/Flaque/filet v0.0.0-20201012163910-45f684403088 7 | github.com/mitchellh/go-homedir v1.1.0 8 | github.com/spf13/afero v1.5.1 // indirect 9 | github.com/stretchr/testify v1.6.1 10 | gopkg.in/h2non/gock.v1 v1.0.16 11 | ) 12 | -------------------------------------------------------------------------------- /v1/go.sum: -------------------------------------------------------------------------------- 1 | github.com/Flaque/filet v0.0.0-20201012163910-45f684403088 h1:PnnQln5IGbhLeJOi6hVs+lCeF+B1dRfFKPGXUAez0Ww= 2 | github.com/Flaque/filet v0.0.0-20201012163910-45f684403088/go.mod h1:TK+jB3mBs+8ZMWhU5BqZKnZWJ1MrLo8etNVg51ueTBo= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= 6 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= 7 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 8 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 9 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 10 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= 11 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 12 | github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/spf13/afero v1.5.1 h1:VHu76Lk0LSP1x254maIu2bplkWpfBWI+B+6fdoZprcg= 16 | github.com/spf13/afero v1.5.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= 17 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 18 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 19 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 20 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 21 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 22 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 23 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 24 | golang.org/x/net v0.0.0-20191021144547-ec77196f6094/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 25 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 26 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 27 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 28 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 29 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 30 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/h2non/gentleman.v1 v1.0.4/go.mod h1:JYuHVdFzS4MKOXe0o+chKJ4hCe6tqKKw9XH9YP6WFrg= 33 | gopkg.in/h2non/gock.v1 v1.0.16 h1:F11k+OafeuFENsjei5t2vMTSTs9L62AdyTe4E1cgdG8= 34 | gopkg.in/h2non/gock.v1 v1.0.16/go.mod h1:XVuDAssexPLwgxCLMvDTWNU5eqklsydR6I5phZ9oPB8= 35 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 36 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 37 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 38 | -------------------------------------------------------------------------------- /v1/upload.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "mime/multipart" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | 14 | "github.com/mitchellh/go-homedir" 15 | ) 16 | 17 | // postFile creates a form file and posts it. 18 | func postFile(filepath string, targetURL string) (*http.Response, error) { 19 | bodyBuf := &bytes.Buffer{} 20 | bodyWriter := multipart.NewWriter(bodyBuf) 21 | 22 | fileWriter, err := bodyWriter.CreateFormFile("file", filepath) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | fh, err := os.Open(filepath) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | _, err = io.Copy(fileWriter, fh) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | contentType := bodyWriter.FormDataContentType() 38 | bodyWriter.Close() 39 | 40 | return http.Post(targetURL, contentType, bodyBuf) 41 | } 42 | 43 | // Upload posts a file using the direct upload method. 44 | func (c *Client) Upload(ctx context.Context, filepath string, params url.Values, v interface{}) error { 45 | // declare an empty interface 46 | var result map[string]interface{} 47 | 48 | err := c.MakeRequest(ctx, http.MethodPost, "/videos/create/", params, &result) 49 | 50 | if err != nil { 51 | return err 52 | } 53 | 54 | if result["status"] != "ok" { 55 | return fmt.Errorf("Error creating video: %s", result["message"]) 56 | } 57 | 58 | link := result["link"].(map[string]interface{}) 59 | 60 | // create upload URL 61 | uploadURL, err := url.Parse("https://" + fmt.Sprintf("%v%v", link["address"], link["path"])) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | values := url.Values{} 67 | query := link["query"].(map[string]interface{}) 68 | // create query paramaters from map 69 | for k, v := range query { 70 | values.Set(k, fmt.Sprint(v)) 71 | } 72 | 73 | // add query string 74 | uploadURL.RawQuery = values.Encode() + "&api_format=json" 75 | 76 | abspath, err := homedir.Expand(filepath) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | // upload file 82 | resp, err := postFile(abspath, uploadURL.String()) 83 | if err != nil { 84 | return err 85 | } 86 | defer resp.Body.Close() 87 | 88 | return json.NewDecoder(resp.Body).Decode(v) 89 | } 90 | -------------------------------------------------------------------------------- /v1/upload_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "testing" 7 | 8 | filet "github.com/Flaque/filet" 9 | "github.com/stretchr/testify/assert" 10 | "gopkg.in/h2non/gock.v1" 11 | ) 12 | 13 | func TestClient_Upload(t *testing.T) { 14 | defer gock.Off() // flush pending mocks after test execution 15 | defer filet.CleanUp(t) // clean up temporary files 16 | 17 | gock.New("https://api.jwplatform.com"). 18 | Post("/v1/videos/create"). 19 | Reply(200). 20 | JSON([]byte(`{"status": "ok", "media": {"type": "video", "key": "8TjWBMMX"}, "link": {"path": "/v1/videos/upload", "query": {"token": "86180b3b11f750a5598a16e5a4e852416ba9f09f78d", "key": "8TjWBMMX"}, "protocol": "http", "address": "upload.jwplatform.com"}, "rate_limit": {"reset": 1575813660, "limit": 60, "remaining": 59}}`)) 21 | 22 | // create a temporary file with no parent dir 23 | file := filet.TmpFile(t, "", "") 24 | 25 | gock.New("https://upload.jwplatform.com"). 26 | Post("/v1/videos/upload"). 27 | File(file.Name()). 28 | Reply(200). 29 | JSON(map[string]string{"status": "ok"}) 30 | 31 | ctx, cancel := context.WithCancel(context.Background()) 32 | defer cancel() 33 | 34 | // set URL params 35 | params := url.Values{} 36 | params.Set("title", "Your video title") 37 | params.Set("description", "Your video description") 38 | 39 | client := NewClient("API_KEY", "API_SECRET") 40 | 41 | // declare an empty interface 42 | var result map[string]interface{} 43 | 44 | // upload video usind direct upload method 45 | err := client.Upload(ctx, file.Name(), params, &result) 46 | if err != nil { 47 | panic(err) 48 | } 49 | 50 | assert.Equal(t, "ok", result["status"]) 51 | } 52 | -------------------------------------------------------------------------------- /v1/v1.go: -------------------------------------------------------------------------------- 1 | /* 2 | Deprecated package providing a client to talk to the V1 JW Platform API. 3 | 4 | import ( 5 | "github.com/jwplayer/jwplatform-go/v1" 6 | ) 7 | 8 | client := v1.NewClient("API_KEY", "API_SECRET") 9 | */ 10 | package v1 11 | 12 | import ( 13 | "context" 14 | "crypto/sha1" 15 | "encoding/hex" 16 | "encoding/json" 17 | "fmt" 18 | "math/rand" 19 | "net/http" 20 | "net/url" 21 | "path" 22 | "sort" 23 | "strconv" 24 | "strings" 25 | "time" 26 | ) 27 | 28 | const version = "1.0.0" 29 | 30 | // Client represents the JWPlatform v1 client object. 31 | type Client struct { 32 | APIVersion string 33 | Version string 34 | BaseURL *url.URL 35 | apiKey string 36 | apiSecret string 37 | httpClient *http.Client 38 | } 39 | 40 | // NewClient creates a V1 new client object. 41 | func NewClient(apiKey string, apiSecret string) *Client { 42 | return &Client{ 43 | APIVersion: "v1", 44 | Version: version, 45 | BaseURL: &url.URL{ 46 | Scheme: "https", 47 | Host: "api.jwplatform.com", 48 | }, 49 | apiKey: apiKey, 50 | apiSecret: apiSecret, 51 | httpClient: http.DefaultClient, 52 | } 53 | } 54 | 55 | // generateNonce generates a random 8 digit as a string. 56 | func generateNonce() string { 57 | rand.Seed(time.Now().UTC().UnixNano()) 58 | return fmt.Sprintf("%08d", rand.Intn(100000000)) 59 | } 60 | 61 | // makeTimestamp gets the unix timestamp in seconds. 62 | func makeTimestamp() int64 { 63 | return time.Now().UnixNano() / (int64(time.Second) / int64(time.Nanosecond)) 64 | } 65 | 66 | // buildParams generates all parameters for api request. 67 | func (c *Client) buildParams(params url.Values) url.Values { 68 | if params == nil { 69 | params = url.Values{} 70 | } 71 | params.Set("api_nonce", generateNonce()) 72 | params.Set("api_key", c.apiKey) 73 | params.Set("api_format", "json") 74 | params.Set("api_timestamp", strconv.FormatInt(makeTimestamp(), 10)) 75 | 76 | // create sorted keys array 77 | var keys []string 78 | for k := range params { 79 | keys = append(keys, k) 80 | } 81 | sort.Strings(keys) 82 | 83 | // construct signature base string 84 | var sbs strings.Builder 85 | for i, k := range keys { 86 | if i != 0 { 87 | sbs.WriteString("&") 88 | } 89 | // iterate over values of type []string 90 | for _, val := range params[k] { 91 | sbs.WriteString(k) 92 | sbs.WriteString("=") 93 | sbs.WriteString(val) 94 | } 95 | } 96 | sbs.WriteString(c.apiSecret) 97 | 98 | // hash signature base string 99 | h := sha1.New() 100 | h.Write([]byte(sbs.String())) 101 | sha := hex.EncodeToString(h.Sum(nil)) 102 | 103 | params.Set("api_signature", sha) 104 | 105 | return params 106 | } 107 | 108 | // newRequestWithContext creates a new request with signed params. 109 | func (c *Client) newRequestWithContext(ctx context.Context, method, pathPart string, params url.Values) (*http.Request, error) { 110 | rel := &url.URL{Path: path.Join(c.APIVersion, pathPart)} 111 | absoluteURL := c.BaseURL.ResolveReference(rel) 112 | absoluteURL.RawQuery = c.buildParams(params).Encode() 113 | 114 | req, err := http.NewRequestWithContext(ctx, method, absoluteURL.String(), nil) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | userAgent := fmt.Sprintf("jwplatform-go/%s", c.Version) 120 | 121 | req.Header.Set("Accept", "application/json") 122 | req.Header.Add("User-Agent", userAgent) 123 | 124 | return req, nil 125 | } 126 | 127 | // do executes request and decodes response body. 128 | func (c *Client) do(req *http.Request, v interface{}) error { 129 | resp, err := c.httpClient.Do(req) 130 | if err != nil { 131 | return err 132 | } 133 | defer resp.Body.Close() 134 | 135 | return json.NewDecoder(resp.Body).Decode(v) 136 | } 137 | 138 | // MakeRequest requests with api signature and decodes json result. 139 | func (c *Client) MakeRequest(ctx context.Context, method, pathPart string, params url.Values, v interface{}) error { 140 | req, err := c.newRequestWithContext(ctx, method, pathPart, params) 141 | if err != nil { 142 | return err 143 | } 144 | 145 | return c.do(req, &v) 146 | } 147 | -------------------------------------------------------------------------------- /v1/v1_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/url" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "gopkg.in/h2non/gock.v1" 11 | ) 12 | 13 | func TestClient_BuildParams(t *testing.T) { 14 | client := NewClient("API_KEY", "API_SECRET") 15 | 16 | // set URL params 17 | params := url.Values{} 18 | params.Set("video_key", "VIDEO_KEY") 19 | 20 | rawQuery := client.buildParams(params).Encode() 21 | 22 | m, err := url.ParseQuery(rawQuery) 23 | 24 | assert.Nil(t, err) 25 | assert.Equal(t, []string{"json"}, m["api_format"]) 26 | assert.Equal(t, []string{"API_KEY"}, m["api_key"]) 27 | assert.Equal(t, []string{"VIDEO_KEY"}, m["video_key"]) 28 | assert.Contains(t, m, "api_nonce") 29 | assert.Contains(t, m, "api_signature") 30 | assert.Contains(t, m, "api_timestamp") 31 | } 32 | 33 | func TestClient_MakeRequest(t *testing.T) { 34 | defer gock.Off() // flush pending mocks after test execution 35 | 36 | gock.New("https://api.jwplatform.com"). 37 | Get("/v1/videos/show"). 38 | MatchParam("video_key", "VIDEO_KEY"). 39 | Reply(200). 40 | JSON(map[string]string{"status": "ok"}) 41 | 42 | ctx, cancel := context.WithCancel(context.Background()) 43 | defer cancel() 44 | 45 | client := NewClient("API_KEY", "API_SECRET") 46 | 47 | // set URL params 48 | params := url.Values{} 49 | params.Set("video_key", "VIDEO_KEY") // some video key, e.g. gIRtMhYM 50 | 51 | // declare an empty interface 52 | var result map[string]interface{} 53 | 54 | client.MakeRequest(ctx, http.MethodGet, "/videos/show/", params, &result) 55 | 56 | assert.Equal(t, "ok", result["status"]) 57 | } 58 | -------------------------------------------------------------------------------- /webhooks.go: -------------------------------------------------------------------------------- 1 | package jwplatform 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/google/go-querystring/query" 8 | ) 9 | 10 | // WebhookResource is the resource that is returned for all Webhook resource requests, 11 | // with the exception of the Create action, which extends this struct with upload-related data. 12 | type WebhookResource struct { 13 | V2ResourceResponse 14 | Metadata WebhookMetadata `json:"metadata"` 15 | } 16 | 17 | // CreateWebhookResponse is the response structure for Webhook create calls. 18 | // 19 | // The Secret is returned only on Create calls and can be used to authenticate incoming webhooks 20 | // Please see the documentation for additional details: 21 | // https://developer.jwplayer.com/jwplayer/docs/learn-about-webhooks#section-verify-the-authenticity-of-a-webhook 22 | type CreateWebhookResponse struct { 23 | V2ResourceResponse 24 | Metadata WebhookMetadata `json:"metadata"` 25 | Secret string `json:"secret"` 26 | } 27 | 28 | // WebhookWriteRequest is the request structure required for Webhook create and update calls. 29 | type WebhookWriteRequest struct { 30 | Metadata WebhookMetadata `json:"metadata"` 31 | } 32 | 33 | // WebhookMetadata describes a Webhook resource 34 | type WebhookMetadata struct { 35 | Name string `json:"name"` 36 | Description string `json:"description,omitempty"` 37 | Events []string `json:"events"` 38 | Sites []string `json:"site_ids"` 39 | WebhookURL string `json:"webhook_url"` 40 | } 41 | 42 | // WebhookResourcesResponse is the response structure for Webhook list calls. 43 | type WebhookResourcesResponse struct { 44 | V2ResourcesResponse 45 | Webhooks []WebhookResource `json:"webhooks"` 46 | } 47 | 48 | // WebhooksClient for interacting with V2 Webhooks API. 49 | type WebhooksClient struct { 50 | v2Client *V2Client 51 | } 52 | 53 | // Get a single Webhook resource by ID. 54 | func (c *WebhooksClient) Get(webhookID string) (*WebhookResource, error) { 55 | webhook := &WebhookResource{} 56 | path := fmt.Sprintf("/v2/webhooks/%s", webhookID) 57 | err := c.v2Client.Request(http.MethodGet, path, webhook, nil, nil) 58 | return webhook, err 59 | } 60 | 61 | // Create a Webhook resource. 62 | func (c *WebhooksClient) Create(webhookMetadata *WebhookMetadata) (*CreateWebhookResponse, error) { 63 | createRequestData := &WebhookWriteRequest{Metadata: *webhookMetadata} 64 | webhook := &CreateWebhookResponse{} 65 | err := c.v2Client.Request(http.MethodPost, "/v2/webhooks", webhook, createRequestData, nil) 66 | return webhook, err 67 | } 68 | 69 | // List all Webhook resources. 70 | func (c *WebhooksClient) List(queryParams *QueryParams) (*WebhookResourcesResponse, error) { 71 | webhooks := &WebhookResourcesResponse{} 72 | urlValues, _ := query.Values(queryParams) 73 | err := c.v2Client.Request(http.MethodGet, "/v2/webhooks", webhooks, nil, urlValues) 74 | return webhooks, err 75 | } 76 | 77 | // Update a Webhook resource by ID. 78 | func (c *WebhooksClient) Update(webhookID string, webhookMetadata *WebhookMetadata) (*WebhookResource, error) { 79 | updateRequestData := &WebhookWriteRequest{Metadata: *webhookMetadata} 80 | webhook := &WebhookResource{} 81 | path := fmt.Sprintf("/v2/webhooks/%s", webhookID) 82 | err := c.v2Client.Request(http.MethodPatch, path, webhook, updateRequestData, nil) 83 | return webhook, err 84 | } 85 | 86 | // Delete a Webhook resource by ID. 87 | func (c *WebhooksClient) Delete(webhookID string) error { 88 | path := fmt.Sprintf("/v2/webhooks/%s", webhookID) 89 | err := c.v2Client.Request(http.MethodDelete, path, nil, nil, nil) 90 | return err 91 | } 92 | -------------------------------------------------------------------------------- /webhooks_test.go: -------------------------------------------------------------------------------- 1 | package jwplatform 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "gopkg.in/h2non/gock.v1" 11 | ) 12 | 13 | func TestGetWebhook(t *testing.T) { 14 | defer gock.Off() 15 | 16 | webhookID := "mnbvcxkj" 17 | mockAuthToken := "shhh" 18 | 19 | requestPath := fmt.Sprintf("/v2/webhooks/%s", webhookID) 20 | mockWebhookResponse := map[string]string{"id": webhookID} 21 | 22 | gock.New("https://api.jwplayer.com"). 23 | Get(requestPath). 24 | MatchHeader("Authorization", "^Bearer .+"). 25 | MatchHeader("User-Agent", "^jwplatform-go/+"). 26 | Reply(200). 27 | JSON(mockWebhookResponse) 28 | 29 | testClient := New(mockAuthToken) 30 | webhook, err := testClient.Webhooks.Get(webhookID) 31 | assert.Equal(t, webhookID, webhook.ID) 32 | assert.Equal(t, nil, err) 33 | } 34 | 35 | func TestDeleteWebhook(t *testing.T) { 36 | defer gock.Off() 37 | 38 | webhookID := "mnbvcxkj" 39 | mockAuthToken := "shhh" 40 | 41 | requestPath := fmt.Sprintf("/v2/webhooks/%s", webhookID) 42 | 43 | gock.New("https://api.jwplayer.com"). 44 | Delete(requestPath). 45 | MatchHeader("Authorization", "^Bearer .+"). 46 | MatchHeader("User-Agent", "^jwplatform-go/+"). 47 | Reply(204) 48 | 49 | testClient := New(mockAuthToken) 50 | err := testClient.Webhooks.Delete(webhookID) 51 | assert.Equal(t, nil, err) 52 | } 53 | 54 | func TestCreateWebhook(t *testing.T) { 55 | defer gock.Off() 56 | 57 | webhookID := "mnbvcxkj" 58 | mockAuthToken := "shhh" 59 | 60 | mockWebhookResponse := map[string]string{"id": webhookID} 61 | 62 | gock.New("https://api.jwplayer.com"). 63 | Post("/v2/webhooks"). 64 | MatchHeader("Authorization", "^Bearer .+"). 65 | MatchHeader("User-Agent", "^jwplatform-go/+"). 66 | Reply(201). 67 | JSON(mockWebhookResponse) 68 | 69 | testClient := New(mockAuthToken) 70 | newWebhook := &WebhookMetadata{Name: "My first webhook", Sites: []string{"abcdefgh"}} 71 | webhook, err := testClient.Webhooks.Create(newWebhook) 72 | assert.Equal(t, webhookID, webhook.ID) 73 | assert.Equal(t, nil, err) 74 | } 75 | 76 | func TestUpdateWebhook(t *testing.T) { 77 | defer gock.Off() 78 | 79 | webhookID := "mnbvcxkj" 80 | mockAuthToken := "shhh" 81 | 82 | requestPath := fmt.Sprintf("/v2/webhooks/%s", webhookID) 83 | mockWebhookResponse := map[string]string{"id": webhookID} 84 | 85 | gock.New("https://api.jwplayer.com"). 86 | Patch(requestPath). 87 | MatchHeader("Authorization", "^Bearer .+"). 88 | MatchHeader("User-Agent", "^jwplatform-go/+"). 89 | Reply(200). 90 | JSON(mockWebhookResponse) 91 | 92 | testClient := New(mockAuthToken) 93 | updateMetadata := &WebhookMetadata{Name: "My first webhook", Sites: []string{"abcdefgh"}} 94 | webhook, err := testClient.Webhooks.Update(webhookID, updateMetadata) 95 | assert.Equal(t, webhookID, webhook.ID) 96 | assert.Equal(t, nil, err) 97 | } 98 | 99 | func TestListWebhooks(t *testing.T) { 100 | defer gock.Off() 101 | 102 | webhookID := "mnbvcxkj" 103 | mockAuthToken := "shhh" 104 | page := 2 105 | pageLength := 4 106 | mockWebhooksResponse := map[string]interface{}{ 107 | "page_length": pageLength, 108 | "page": page, 109 | "webhooks": []map[string]string{{"id": webhookID}}, 110 | } 111 | 112 | gock.New("https://api.jwplayer.com"). 113 | Get("/v2/webhooks"). 114 | MatchHeader("Authorization", "^Bearer .+"). 115 | MatchHeader("User-Agent", "^jwplatform-go/+"). 116 | MatchParam("page", strconv.Itoa(page)). 117 | MatchParam("page_length", strconv.Itoa(pageLength)). 118 | Reply(200). 119 | JSON(mockWebhooksResponse) 120 | 121 | testClient := New(mockAuthToken) 122 | params := &QueryParams{PageLength: pageLength, Page: page} 123 | webhooksResponse, err := testClient.Webhooks.List(params) 124 | assert.Equal(t, page, webhooksResponse.Page) 125 | assert.Equal(t, pageLength, webhooksResponse.PageLength) 126 | assert.Equal(t, webhookID, webhooksResponse.Webhooks[0].ID) 127 | assert.Equal(t, nil, err) 128 | } 129 | 130 | func TestUnmarshalWebhook(t *testing.T) { 131 | webhookData := map[string]interface{}{ 132 | "id": "abZqokMz", 133 | "type": "webhook", 134 | "created": "2019-09-25T15:29:11.042095+00:00", 135 | "last_modified": "2019-09-25T15:29:11.042095+00:00", 136 | "metadata": map[string]interface{}{ 137 | "name": "Webhook", 138 | "description": "Describes a webhook", 139 | "webhook_url": "https://webhook.com", 140 | "site_ids": []string{"a", "b"}, 141 | "events": []string{"event_a", "event_b"}, 142 | }, 143 | } 144 | 145 | bytes, err := json.Marshal(&webhookData) 146 | assert.NoError(t, err) 147 | 148 | var webhook WebhookResource 149 | err = json.Unmarshal(bytes, &webhook) 150 | assert.NoError(t, err) 151 | 152 | assert.Equal(t, "abZqokMz", webhook.ID) 153 | assert.Equal(t, "webhook", webhook.Type) 154 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", webhook.Created) 155 | assert.Equal(t, "2019-09-25T15:29:11.042095+00:00", webhook.LastModified) 156 | assert.Equal(t, "Webhook", webhook.Metadata.Name) 157 | assert.Equal(t, "Describes a webhook", webhook.Metadata.Description) 158 | assert.Equal(t, "https://webhook.com", webhook.Metadata.WebhookURL) 159 | assert.Equal(t, []string{"a", "b"}, webhook.Metadata.Sites) 160 | assert.Equal(t, []string{"event_a", "event_b"}, webhook.Metadata.Events) 161 | } 162 | --------------------------------------------------------------------------------