├── .github
└── workflows
│ ├── publish.yml
│ └── push.yml
├── .gitignore
├── .node-version
├── .npmignore
├── LICENSE
├── README.md
├── jest.config.js
├── package.json
├── src
├── client
│ ├── index.ts
│ └── types.ts
├── constants.ts
├── eventEmitter.ts
├── index.ts
├── interceptors
│ └── response
│ │ ├── clientIPChangeEmitter.spec.ts
│ │ ├── clientIPChangeEmitter.ts
│ │ ├── errorEmitter.spec.ts
│ │ ├── errorEmitter.ts
│ │ ├── responseFormatter.spec.ts
│ │ └── responseFormatter.ts
├── resources
│ ├── Account
│ │ ├── Account.ts
│ │ └── types.ts
│ ├── Auth
│ │ ├── Auth.ts
│ │ ├── OAuth.ts
│ │ └── types.ts
│ ├── Config.ts
│ ├── DownloadLinks
│ │ ├── DownloadLinks.ts
│ │ └── types.ts
│ ├── Events
│ │ ├── Events.ts
│ │ └── types.ts
│ ├── Files
│ │ ├── File.spec.ts
│ │ ├── File.ts
│ │ ├── Files.spec.ts
│ │ ├── Files.ts
│ │ └── types.ts
│ ├── FriendInvites
│ │ ├── FriendInvites.ts
│ │ └── types.ts
│ ├── Friends
│ │ ├── Friends.ts
│ │ └── types.ts
│ ├── IFTTT.ts
│ ├── Payment
│ │ ├── Payment.ts
│ │ └── types.ts
│ ├── RSS
│ │ ├── RSS.ts
│ │ └── types.ts
│ ├── Sharing
│ │ ├── Sharing.ts
│ │ └── types.ts
│ ├── Transfers
│ │ ├── Transfers.ts
│ │ └── types.ts
│ ├── Trash.ts
│ ├── Tunnel.ts
│ ├── User
│ │ ├── User.ts
│ │ └── types.ts
│ └── Zips.ts
├── test-utils
│ └── mocks.ts
└── utils
│ ├── index.ts
│ ├── types.ts
│ └── utils.spec.ts
├── tsconfig.json
└── yarn.lock
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish package to npm
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | jobs:
8 | publish-npm:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 | - uses: actions/setup-node@v3
13 | with:
14 | node-version-file: .node-version
15 | registry-url: https://registry.npmjs.org/
16 | - run: yarn install
17 | - run: yarn lint
18 | - run: yarn test
19 | - run: yarn publish
20 | env:
21 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
22 |
--------------------------------------------------------------------------------
/.github/workflows/push.yml:
--------------------------------------------------------------------------------
1 | name: Lint, test, and build the package
2 |
3 | on: push
4 |
5 | jobs:
6 | build:
7 | name: Build, lint, and test
8 |
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - name: Checkout repo
13 | uses: actions/checkout@v3
14 |
15 | - uses: actions/setup-node@v3
16 | with:
17 | node-version-file: .node-version
18 |
19 | - name: Install deps and build (with cache)
20 | uses: bahmutov/npm-install@v1
21 |
22 | - name: Lint
23 | run: yarn lint
24 |
25 | - name: Test
26 | run: yarn test --coverage
27 |
28 | - name: Coveralls
29 | uses: coverallsapp/github-action@v2
30 |
31 | - name: Build
32 | run: yarn build
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | dist
4 | *.log
5 | coverage
6 | .DS_Store
7 | demo
8 |
--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
1 | 20.10.0
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache
3 | .DS_Store
4 | example
5 | demo
6 | src
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 put.io
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
putio-js
7 |
8 |
9 | JavaScript SDK for interacting with the put.io API.
10 |
11 |
12 |
13 |
16 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | ## Installation
25 |
26 | ```bash
27 | yarn add @putdotio/api-client
28 |
29 | npm install @putdotio/api-client
30 | ```
31 |
32 | #### ES Modules / TypeScript
33 |
34 | ```ts
35 | import PutioAPI from '@putdotio/api-client'
36 | ```
37 |
38 | #### CommonJS
39 |
40 | ```js
41 | const PutioAPI = require('@putdotio/api-client').default
42 | ```
43 |
44 | ## Usage
45 |
46 | ```ts
47 | // you can pass the options in constructor
48 | const putioAPI = new PutioAPI({ clientID: 'OAUTH_CLIENT_ID' })
49 |
50 | // or use `configure` method
51 | MyApp.bootstrap(config => {
52 | putioAPI.configure({ clientID: config.OAUTH_CLIENT_ID })
53 | })
54 |
55 | // setToken will send the given auth token with every request, in Authorization header
56 | MyApp.onLogin(token => {
57 | putioAPI.setToken(token)
58 |
59 | putioAPI.Account.Info()
60 | .then(r => console.log('Fetched user info: ', r))
61 | .catch(e => console.log('An error occurred while fetching user info: ', e))
62 | })
63 |
64 | // clearToken will perform a clean-up and stop sending the token in Authorization header
65 | MyApp.onLogout(() => {
66 | putioAPI.clearToken()
67 | })
68 | ```
69 |
70 | ## API
71 |
72 | ### Options
73 |
74 | | Prop | Type | Default Value | Description |
75 | | :------------ | :----- | :------------------------------------- | :-------------------------------------------------------------------- |
76 | | **clientID** | number | 1 | OAuth app client ID, defaults to [put.io web app](https://app.put.io) |
77 | | **baseURL** | string | [api.put.io/v2](https://api.put.io/v2) | Base URL of the API |
78 | | **webAppURL** | string | [app.put.io](https://app.put.io) | Base URL of the Put.io web app, used in the authentication flow |
79 |
80 | ### Methods
81 |
82 | | Name | Parameters | Return Value |
83 | | :------------- | :---------------------------------- | :---------------------- |
84 | | **configure** | `(options: IPutioAPIClientOptions)` | PutioAPIClient Instance |
85 | | **setToken** | `(token: string)` | PutioAPIClient Instance |
86 | | **clearToken** | | PutioAPIClient Instance |
87 |
88 | ### Events
89 |
90 | | Value | Payload | Description |
91 | | :-------------------- | :--------------------------------------------------------------------------------------------------- | :-------------------------------------- |
92 | | **ERROR** | [IPutioAPIClientError](https://github.com/putdotio/putio-js/blob/master/src/client/types.ts#L22-L26) | Fired when an HTTP request fails |
93 | | **CLIENT_IP_CHANGED** | `{ IP: string, newIP: string }` | Fired when the IP of the client changes |
94 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | collectCoverageFrom: ['./src/interceptors/**', './src/utils/**'],
3 | }
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "put.io ",
3 | "bugs": {
4 | "url": "https://github.com/putdotio/putio-js/issues"
5 | },
6 | "dependencies": {
7 | "axios": "^0.21.1",
8 | "event-emitter": "^0.3.5",
9 | "js-base64": "2.6.3",
10 | "qs": "^6.10.3",
11 | "urijs": "^1.19.7"
12 | },
13 | "description": "JavaScript SDK for interacting with the put.io API.",
14 | "devDependencies": {
15 | "@putdotio/prettier-config": "^1.0.0",
16 | "@types/event-emitter": "^0.3.3",
17 | "@types/js-base64": "2.3.1",
18 | "@types/qs": "^6.9.7",
19 | "@types/urijs": "^1.19.18",
20 | "@types/uuid": "^8.3.4",
21 | "husky": "^4.2.5",
22 | "tsdx": "^0.14.1",
23 | "tslib": "^2.3.1",
24 | "typescript": "4.5.5"
25 | },
26 | "engines": {
27 | "node": ">=10"
28 | },
29 | "files": [
30 | "dist",
31 | "src"
32 | ],
33 | "homepage": "https://github.com/putdotio/putio-js",
34 | "husky": {
35 | "hooks": {
36 | "pre-commit": "tsdx lint"
37 | }
38 | },
39 | "keywords": [
40 | "putio",
41 | "put.io",
42 | "sdk"
43 | ],
44 | "license": "MIT",
45 | "main": "dist/index.js",
46 | "module": "dist/api-client.esm.js",
47 | "name": "@putdotio/api-client",
48 | "prettier": "@putdotio/prettier-config",
49 | "publishConfig": {
50 | "access": "public"
51 | },
52 | "repository": {
53 | "type": "git",
54 | "url": "https://github.com/putdotio/putio-js.git"
55 | },
56 | "resolutions": {
57 | "**/@typescript-eslint/eslint-plugin": "5.10.2",
58 | "**/@typescript-eslint/parser": "5.10.2"
59 | },
60 | "scripts": {
61 | "build": "tsdx build",
62 | "lint": "tsdx lint",
63 | "prepare": "tsdx build",
64 | "start": "tsdx watch",
65 | "test": "tsdx test",
66 | "tsc": "tsc --noEmit"
67 | },
68 | "typings": "dist/index.d.ts",
69 | "version": "8.49.0"
70 | }
71 |
--------------------------------------------------------------------------------
/src/client/index.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
2 | import qs from 'qs'
3 |
4 | import {
5 | PutioAPIClientResponseInterceptorFactory,
6 | IPutioAPIClientOptions,
7 | IPutioAPIClientResponse,
8 | } from './types'
9 |
10 | import { DEFAULT_CLIENT_OPTIONS } from '../constants'
11 |
12 | import { createClientIPChangeEmitter } from '../interceptors/response/clientIPChangeEmitter'
13 | import { createErrorEmitter } from '../interceptors/response/errorEmitter'
14 | import { createResponseFormatter } from '../interceptors/response/responseFormatter'
15 |
16 | import {
17 | eventEmitter,
18 | EVENTS,
19 | PutioAPIClientEventTypes,
20 | EventListener,
21 | } from '../eventEmitter'
22 |
23 | import Account from '../resources/Account/Account'
24 | import Auth from '../resources/Auth/Auth'
25 | import OAuth from '../resources/Auth/OAuth'
26 | import DownloadLinks from '../resources/DownloadLinks/DownloadLinks'
27 | import Sharing from '../resources/Sharing/Sharing'
28 | import Config from '../resources/Config'
29 | import Events from '../resources/Events/Events'
30 | import File from '../resources/Files/File'
31 | import Files from '../resources/Files/Files'
32 | import FriendInvites from '../resources/FriendInvites/FriendInvites'
33 | import Friends from '../resources/Friends/Friends'
34 | import IFTTT from '../resources/IFTTT'
35 | import Payment from '../resources/Payment/Payment'
36 | import RSS from '../resources/RSS/RSS'
37 | import Transfers from '../resources/Transfers/Transfers'
38 | import Trash from '../resources/Trash'
39 | import Tunnel from '../resources/Tunnel'
40 | import User from '../resources/User/User'
41 | import Zips from '../resources/Zips'
42 |
43 | export class PutioAPIClient {
44 | public static EVENTS = EVENTS
45 | public static DEFAULT_OPTIONS = DEFAULT_CLIENT_OPTIONS
46 |
47 | public options: IPutioAPIClientOptions
48 | public token: string | undefined
49 | public http: AxiosInstance
50 |
51 | public Account: Account
52 | public Auth: Auth
53 | public DownloadLinks: DownloadLinks
54 | public Sharing: Sharing
55 | public Config: Config
56 | public Events: Events
57 | public Files: Files
58 | public File: File
59 | public Friends: Friends
60 | public FriendInvites: FriendInvites
61 | public IFTTT: IFTTT
62 | public OAuth: OAuth
63 | public Payment: Payment
64 | public RSS: RSS
65 | public Transfers: Transfers
66 | public Trash: Trash
67 | public Tunnel: Tunnel
68 | public User: User
69 | public Zips: Zips
70 |
71 | constructor(options: IPutioAPIClientOptions) {
72 | this.options = { ...PutioAPIClient.DEFAULT_OPTIONS, ...options }
73 | this.http = this.createHTTPClient()
74 |
75 | this.Account = new Account(this)
76 | this.Auth = new Auth(this)
77 | this.DownloadLinks = new DownloadLinks(this)
78 | this.Sharing = new Sharing(this)
79 | this.Config = new Config(this)
80 | this.Events = new Events(this)
81 | this.Files = new Files(this)
82 | this.File = new File(this)
83 | this.Friends = new Friends(this)
84 | this.FriendInvites = new FriendInvites(this)
85 | this.OAuth = new OAuth(this)
86 | this.Payment = new Payment(this)
87 | this.RSS = new RSS(this)
88 | this.Transfers = new Transfers(this)
89 | this.Trash = new Trash(this)
90 | this.Tunnel = new Tunnel(this)
91 | this.User = new User(this)
92 | this.Zips = new Zips(this)
93 | this.IFTTT = new IFTTT(this)
94 | }
95 |
96 | public once(event: PutioAPIClientEventTypes, listener: EventListener) {
97 | eventEmitter.once(event, listener)
98 | }
99 |
100 | public on(event: PutioAPIClientEventTypes, listener: EventListener) {
101 | eventEmitter.on(event, listener)
102 | }
103 |
104 | public off(event: PutioAPIClientEventTypes, listener: EventListener) {
105 | eventEmitter.off(event, listener)
106 | }
107 |
108 | public configure(options: IPutioAPIClientOptions) {
109 | this.options = { ...this.options, ...options }
110 | return this
111 | }
112 |
113 | public setToken(token: string): PutioAPIClient {
114 | this.token = token
115 | this.http.defaults.headers.common.Authorization = `token ${token}`
116 | return this
117 | }
118 |
119 | public clearToken(): PutioAPIClient {
120 | this.token = undefined
121 | this.http.defaults.headers.common.Authorization = ``
122 | return this
123 | }
124 |
125 | public get(
126 | url: string,
127 | config?: AxiosRequestConfig,
128 | ): Promise> {
129 | return this.http({
130 | method: 'GET',
131 | url,
132 | ...config,
133 | })
134 | }
135 |
136 | public post(
137 | url: string,
138 | config?: AxiosRequestConfig,
139 | ): Promise> {
140 | return this.http({
141 | method: 'POST',
142 | url,
143 | ...config,
144 | })
145 | }
146 |
147 | public put(
148 | url: string,
149 | config?: AxiosRequestConfig,
150 | ): Promise> {
151 | return this.http({
152 | method: 'PUT',
153 | url,
154 | ...config,
155 | })
156 | }
157 |
158 | public delete(
159 | url: string,
160 | config?: AxiosRequestConfig,
161 | ): Promise> {
162 | return this.http({
163 | method: 'DELETE',
164 | url,
165 | ...config,
166 | })
167 | }
168 |
169 | private createHTTPClient() {
170 | const axiosInstance = axios.create({
171 | baseURL: this.options.baseURL,
172 | withCredentials: true,
173 | timeout: 30000,
174 | paramsSerializer: params =>
175 | qs.stringify(params, { arrayFormat: 'comma' }),
176 | })
177 |
178 | // apply response interceptors
179 | const responseInterceptorFactories: PutioAPIClientResponseInterceptorFactory[] = [
180 | createResponseFormatter,
181 | createClientIPChangeEmitter,
182 | createErrorEmitter,
183 | ]
184 |
185 | responseInterceptorFactories
186 | .map(createResponseInterceptor => createResponseInterceptor(this.options))
187 | .forEach(({ onFulfilled, onRejected }) => {
188 | axiosInstance.interceptors.response.use(onFulfilled, onRejected)
189 | })
190 |
191 | return axiosInstance
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/src/client/types.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError, AxiosResponse } from 'axios'
2 |
3 | export interface IPutioAPIClientOptions {
4 | clientID?: number
5 | baseURL?: string
6 | webAppURL?: string
7 | }
8 |
9 | export interface IPutioAPIClientResponse extends AxiosResponse {
10 | data: T & { status: 'OK' }
11 | body?: T & { status: 'OK' } // @TODO: Remove when it's irrelevant.
12 | }
13 |
14 | export interface IPutioAPIClientErrorData {
15 | 'x-trace-id'?: string
16 | error_id?: string
17 | error_uri?: string
18 | error_type: string
19 | error_message: string
20 | status_code: number
21 | extra: Record
22 | }
23 |
24 | export interface IPutioAPIClientError
25 | extends AxiosError {
26 | data: IPutioAPIClientErrorData
27 | toJSON: () => IPutioAPIClientErrorData
28 | }
29 |
30 | export type PutioAPIClientResponseInterceptor = {
31 | onFulfilled: (
32 | response: IPutioAPIClientResponse,
33 | ) => IPutioAPIClientResponse
34 | onRejected: (error: IPutioAPIClientError) => Promise
35 | }
36 |
37 | export type PutioAPIClientResponseInterceptorFactory = (
38 | options: IPutioAPIClientOptions,
39 | ) => PutioAPIClientResponseInterceptor
40 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | import { IPutioAPIClientOptions } from './client/types'
2 |
3 | export const DEFAULT_CLIENT_OPTIONS: IPutioAPIClientOptions = {
4 | baseURL: 'https://api.put.io/v2',
5 | clientID: 1,
6 | webAppURL: 'https://app.put.io',
7 | }
8 |
--------------------------------------------------------------------------------
/src/eventEmitter.ts:
--------------------------------------------------------------------------------
1 | import EventEmitter from 'event-emitter'
2 |
3 | export { EventListener } from 'event-emitter'
4 |
5 | export const EVENTS = {
6 | ERROR: 'ERROR',
7 | CLIENT_IP_CHANGED: 'CLIENT_IP_CHANGED',
8 | } as const
9 |
10 | export type PutioAPIClientEventTypes = typeof EVENTS[keyof typeof EVENTS]
11 |
12 | export const eventEmitter = EventEmitter()
13 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './test-utils/mocks'
2 | export * from './client/types'
3 | export * from './constants'
4 |
5 | export * from './resources/Account/types'
6 | export * from './resources/Auth/types'
7 | export * from './resources/DownloadLinks/types'
8 | export * from './resources/Events/types'
9 | export * from './resources/Files/types'
10 | export * from './resources/FriendInvites/types'
11 | export * from './resources/Friends/types'
12 | export * from './resources/Payment/types'
13 | export * from './resources/RSS/types'
14 | export * from './resources/Sharing/types'
15 | export * from './resources/Transfers/types'
16 | export * from './resources/User/types'
17 |
18 | export { isPutioAPIError, isPutioAPIErrorResponse } from './utils'
19 | export { PutioAPIClient as default } from './client'
20 |
--------------------------------------------------------------------------------
/src/interceptors/response/clientIPChangeEmitter.spec.ts:
--------------------------------------------------------------------------------
1 | import { eventEmitter, EVENTS } from '../../eventEmitter'
2 | import {
3 | mockPutioAPIClientError,
4 | mockPutioAPIClientResponse,
5 | } from '../../test-utils/mocks'
6 | import {
7 | IPutioAPIClientError,
8 | PutioAPIClientResponseInterceptor,
9 | } from '../../client/types'
10 | import { DEFAULT_CLIENT_OPTIONS } from '../../constants'
11 | import { createClientIPChangeEmitter } from './clientIPChangeEmitter'
12 |
13 | describe('interceptors/response/clientIPChangeEmitter', () => {
14 | const eventEmitterEmit = jest.spyOn(eventEmitter, 'emit')
15 | let clientIPChangeEmitter: PutioAPIClientResponseInterceptor
16 |
17 | beforeEach(() => {
18 | jest.resetAllMocks()
19 | clientIPChangeEmitter = createClientIPChangeEmitter(DEFAULT_CLIENT_OPTIONS)
20 | })
21 |
22 | it('does not call client.emit method if the IP does not change', () => {
23 | clientIPChangeEmitter.onFulfilled(mockPutioAPIClientResponse)
24 | clientIPChangeEmitter.onFulfilled(mockPutioAPIClientResponse)
25 | expect(eventEmitterEmit).not.toHaveBeenCalled()
26 | })
27 |
28 | it('does not call client.emit method if the IP changes from initial state', () => {
29 | clientIPChangeEmitter.onFulfilled(mockPutioAPIClientResponse)
30 | clientIPChangeEmitter.onFulfilled({
31 | ...mockPutioAPIClientResponse,
32 | headers: { 'putio-client-ip': 1 },
33 | })
34 | expect(eventEmitterEmit).not.toHaveBeenCalled()
35 | })
36 |
37 | it('calls client.emit method if the IP changes once', () => {
38 | clientIPChangeEmitter.onFulfilled({
39 | ...mockPutioAPIClientResponse,
40 | headers: { 'putio-client-ip': '198.168.0.1' },
41 | })
42 |
43 | clientIPChangeEmitter.onFulfilled({
44 | ...mockPutioAPIClientResponse,
45 | headers: { 'putio-client-ip': '198.168.0.2' },
46 | })
47 |
48 | expect(eventEmitterEmit).toBeCalledWith(EVENTS.CLIENT_IP_CHANGED, {
49 | IP: '198.168.0.1',
50 | newIP: '198.168.0.2',
51 | })
52 | })
53 |
54 | it('calls client.emit method if the IP changes multiple times', () => {
55 | clientIPChangeEmitter.onFulfilled({
56 | ...mockPutioAPIClientResponse,
57 | headers: { 'putio-client-ip': '198.168.0.1' },
58 | })
59 |
60 | clientIPChangeEmitter.onFulfilled({
61 | ...mockPutioAPIClientResponse,
62 | headers: { 'putio-client-ip': '198.168.0.2' },
63 | })
64 |
65 | expect(eventEmitterEmit).toBeCalledWith(EVENTS.CLIENT_IP_CHANGED, {
66 | IP: '198.168.0.1',
67 | newIP: '198.168.0.2',
68 | })
69 |
70 | clientIPChangeEmitter.onFulfilled({
71 | ...mockPutioAPIClientResponse,
72 | headers: { 'putio-client-ip': '198.168.0.1' },
73 | })
74 |
75 | expect(eventEmitterEmit).toBeCalledWith(EVENTS.CLIENT_IP_CHANGED, {
76 | IP: '198.168.0.2',
77 | newIP: '198.168.0.1',
78 | })
79 |
80 | expect(eventEmitterEmit).toBeCalledTimes(2)
81 | })
82 |
83 | it('calls client.emit method once if the IP changes to a falsy value', () => {
84 | clientIPChangeEmitter.onFulfilled({
85 | ...mockPutioAPIClientResponse,
86 | headers: { 'putio-client-ip': '198.168.0.1' },
87 | })
88 |
89 | clientIPChangeEmitter.onFulfilled({
90 | ...mockPutioAPIClientResponse,
91 | headers: { 'putio-client-ip': '198.168.0.2' },
92 | })
93 |
94 | expect(eventEmitterEmit).toBeCalledWith(EVENTS.CLIENT_IP_CHANGED, {
95 | IP: '198.168.0.1',
96 | newIP: '198.168.0.2',
97 | })
98 |
99 | clientIPChangeEmitter.onFulfilled(mockPutioAPIClientResponse)
100 |
101 | expect(eventEmitterEmit).toBeCalledTimes(1)
102 | })
103 |
104 | it('handles failed requests and updates the IP', () => {
105 | const error: IPutioAPIClientError = {
106 | ...mockPutioAPIClientError,
107 | response: {
108 | config: {},
109 | data: {
110 | error_type: 'API_ERROR',
111 | error_message: 'Putio API Error',
112 | status_code: 400,
113 | extra: { foo: 'bar' },
114 | },
115 | headers: { 'putio-client-ip': '0.0.0.0' },
116 | status: 400,
117 | statusText: 'Error!',
118 | },
119 | }
120 |
121 | clientIPChangeEmitter.onRejected(error).catch(e => expect(e).toEqual(error))
122 |
123 | clientIPChangeEmitter.onFulfilled({
124 | ...mockPutioAPIClientResponse,
125 | headers: { 'putio-client-ip': '1.1.1.1' },
126 | })
127 |
128 | expect(eventEmitterEmit).toBeCalledWith(EVENTS.CLIENT_IP_CHANGED, {
129 | IP: '0.0.0.0',
130 | newIP: '1.1.1.1',
131 | })
132 | })
133 |
134 | it('does not call client.emit method if the error is not recognized', () => {
135 | const error = new Error()
136 |
137 | clientIPChangeEmitter
138 | .onRejected(error as any)
139 | .catch(e => expect(e).toEqual(error))
140 |
141 | expect(eventEmitterEmit).not.toHaveBeenCalled()
142 | })
143 | })
144 |
--------------------------------------------------------------------------------
/src/interceptors/response/clientIPChangeEmitter.ts:
--------------------------------------------------------------------------------
1 | import { AxiosResponse } from 'axios'
2 | import { PutioAPIClientResponseInterceptorFactory } from '../../client/types'
3 | import { eventEmitter, EVENTS } from '../../eventEmitter'
4 |
5 | const IP_HEADER_KEY = 'putio-client-ip'
6 |
7 | export const createClientIPChangeEmitter: PutioAPIClientResponseInterceptorFactory = () => {
8 | let IP: string = ''
9 |
10 | const checkIP = (response: AxiosResponse) => {
11 | const newIP = response.headers[IP_HEADER_KEY]
12 |
13 | if (!IP) {
14 | IP = newIP
15 | return
16 | }
17 |
18 | if (newIP && IP !== newIP) {
19 | eventEmitter.emit(EVENTS.CLIENT_IP_CHANGED, { IP, newIP })
20 | IP = newIP
21 | return
22 | }
23 | }
24 |
25 | return {
26 | onFulfilled: response => {
27 | checkIP(response)
28 | return response
29 | },
30 |
31 | onRejected: error => {
32 | if (error.response) {
33 | checkIP(error.response)
34 | }
35 |
36 | return Promise.reject(error)
37 | },
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/interceptors/response/errorEmitter.spec.ts:
--------------------------------------------------------------------------------
1 | import { eventEmitter, EVENTS } from '../../eventEmitter'
2 | import {
3 | mockPutioAPIClientError,
4 | mockPutioAPIClientResponse,
5 | } from '../../test-utils/mocks'
6 | import { IPutioAPIClientError } from '../../client/types'
7 | import { DEFAULT_CLIENT_OPTIONS } from '../../constants'
8 | import { createErrorEmitter } from './errorEmitter'
9 |
10 | describe('interceptors/response/errorEmitter', () => {
11 | const eventEmitterEmit = jest.spyOn(eventEmitter, 'emit')
12 | const errorEmitter = createErrorEmitter(DEFAULT_CLIENT_OPTIONS)
13 |
14 | beforeEach(jest.resetAllMocks)
15 |
16 | describe('successful responses', () => {
17 | it('does not call client.emit method', () => {
18 | errorEmitter.onFulfilled(mockPutioAPIClientResponse)
19 | expect(eventEmitterEmit).not.toHaveBeenCalled()
20 | })
21 | })
22 |
23 | describe('failed responses', () => {
24 | it('calls client.emit method with correct signature', () => {
25 | const error: IPutioAPIClientError = {
26 | ...mockPutioAPIClientError,
27 | response: {
28 | config: {},
29 | data: {
30 | status_code: 400,
31 | error_type: 'API_ERROR',
32 | error_message: 'Putio API Error',
33 | extra: { foo: 'bar' },
34 | },
35 | headers: {},
36 | status: 400,
37 | statusText: 'Error!',
38 | },
39 | }
40 |
41 | errorEmitter.onRejected(error).catch(e => expect(e).toEqual(error))
42 | expect(eventEmitterEmit).toHaveBeenCalledWith(EVENTS.ERROR, error)
43 | })
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/src/interceptors/response/errorEmitter.ts:
--------------------------------------------------------------------------------
1 | import { PutioAPIClientResponseInterceptorFactory } from '../../client/types'
2 | import { identity } from '../../utils'
3 | import { eventEmitter, EVENTS } from '../../eventEmitter'
4 |
5 | export const createErrorEmitter: PutioAPIClientResponseInterceptorFactory = () => ({
6 | onFulfilled: identity,
7 |
8 | onRejected: error => {
9 | eventEmitter.emit(EVENTS.ERROR, error)
10 | return Promise.reject(error)
11 | },
12 | })
13 |
--------------------------------------------------------------------------------
/src/interceptors/response/responseFormatter.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | mockPutioAPIClientError,
3 | mockPutioAPIClientResponse,
4 | createMockXMLHttpRequest,
5 | mockAxiosError,
6 | } from '../../test-utils/mocks'
7 | import { IPutioAPIClientError } from '../../client/types'
8 | import { DEFAULT_CLIENT_OPTIONS } from '../../constants'
9 | import { createResponseFormatter } from './responseFormatter'
10 |
11 | describe('interceptors/response/responseFormatter', () => {
12 | const responseFormatter = createResponseFormatter(DEFAULT_CLIENT_OPTIONS)
13 |
14 | describe('successful responses', () => {
15 | it('transforms as expected', () => {
16 | expect(responseFormatter.onFulfilled(mockPutioAPIClientResponse))
17 | .toMatchInlineSnapshot(`
18 | Object {
19 | "body": Object {
20 | "foo": "bar",
21 | "status": "OK",
22 | },
23 | "config": Object {},
24 | "data": Object {
25 | "foo": "bar",
26 | "status": "OK",
27 | },
28 | "headers": Object {
29 | "x-trace-id": "MOCK_TRACE_ID",
30 | },
31 | "status": 200,
32 | "statusText": "ok",
33 | }
34 | `)
35 | })
36 | })
37 |
38 | describe('failed responses', () => {
39 | it('sets error.data property correctly when the request failed with client timeout error', () => {
40 | const mockAxiosErrorWithTimeout = {
41 | ...mockAxiosError,
42 | code: 'ECONNABORTED',
43 | }
44 |
45 | const mockPutioAPIClientErrorWithTimeout = {
46 | ...mockAxiosErrorWithTimeout,
47 | ...mockPutioAPIClientError,
48 | }
49 |
50 | const error = {
51 | ...mockPutioAPIClientErrorWithTimeout,
52 | response: undefined,
53 | request: undefined,
54 | config: {},
55 | }
56 |
57 | responseFormatter.onRejected(error).catch(e => {
58 | expect(e).toMatchInlineSnapshot(`
59 | Object {
60 | "error_message": "Request timed out",
61 | "error_type": "ERROR",
62 | "extra": Object {},
63 | "status_code": 408,
64 | "x-trace-id": undefined,
65 | }
66 | `)
67 | })
68 | })
69 |
70 | it('sets error.data property correctly when the request failed with put.io API signature', () => {
71 | const error = {
72 | ...mockPutioAPIClientError,
73 | response: {
74 | config: {},
75 | data: {
76 | error_type: 'API_ERROR',
77 | error_message: 'Putio API Error',
78 | status_code: 400,
79 | extra: { foo: 'bar' },
80 | },
81 | headers: {
82 | 'x-trace-id': 'MOCK_TRACE_ID',
83 | },
84 | status: 400,
85 | statusText: 'Error!',
86 | },
87 | }
88 |
89 | responseFormatter.onRejected(error).catch(e =>
90 | expect(e).toMatchInlineSnapshot(`
91 | Object {
92 | "error_message": "Putio API Error",
93 | "error_type": "API_ERROR",
94 | "extra": Object {
95 | "foo": "bar",
96 | },
97 | "status_code": 400,
98 | "x-trace-id": "MOCK_TRACE_ID",
99 | }
100 | `),
101 | )
102 | })
103 |
104 | it('sets error.data property correctly when the request failed with HTTP response but without put.io API signature and no meaningful header config', () => {
105 | const error: IPutioAPIClientError = {
106 | ...mockPutioAPIClientError,
107 | response: {
108 | config: {},
109 | headers: {},
110 | data: 'Bad Gateway',
111 | status: 502,
112 | statusText: 'Bad Gateway',
113 | },
114 | }
115 |
116 | error.config.headers = undefined
117 |
118 | responseFormatter.onRejected(error).catch(e =>
119 | expect(e).toMatchInlineSnapshot(`
120 | Object {
121 | "error_message": "AXIOS_ERROR_MESSAGE",
122 | "error_type": "ERROR",
123 | "extra": Object {},
124 | "status_code": 502,
125 | "x-trace-id": undefined,
126 | }
127 | `),
128 | )
129 | })
130 |
131 | it('sets error.data property correctly when the request failed without put.io API signature', () => {
132 | responseFormatter.onRejected(mockPutioAPIClientError).catch(e =>
133 | expect(e).toMatchInlineSnapshot(`
134 | Object {
135 | "error_message": "AXIOS_ERROR_MESSAGE",
136 | "error_type": "ERROR",
137 | "extra": Object {},
138 | "status_code": 0,
139 | "x-trace-id": undefined,
140 | }
141 | `),
142 | )
143 | })
144 |
145 | it('sets error.data property corrrectly when the request doesnt have response object, but has a request object that is a valid XMLHttpRequest', () => {
146 | const error: IPutioAPIClientError = {
147 | ...mockPutioAPIClientError,
148 | response: undefined,
149 | request: createMockXMLHttpRequest(
150 | 4,
151 | 400,
152 | JSON.stringify({ foo: 'bar' }),
153 | ),
154 | }
155 |
156 | responseFormatter.onRejected(error).catch(e =>
157 | expect(e).toMatchInlineSnapshot(`
158 | Object {
159 | "error_message": "AXIOS_ERROR_MESSAGE",
160 | "error_type": "ERROR",
161 | "extra": Object {},
162 | "foo": "bar",
163 | "status_code": 400,
164 | "x-trace-id": undefined,
165 | }
166 | `),
167 | )
168 | })
169 |
170 | it('sets error.data property corrrectly when the request doesnt have response object, but has a request object that is a valid XMLHttpRequest', () => {
171 | const error: IPutioAPIClientError = {
172 | ...mockPutioAPIClientError,
173 | response: undefined,
174 | request: createMockXMLHttpRequest(
175 | 4,
176 | 500,
177 | 'no meaningful response body',
178 | ),
179 | }
180 |
181 | responseFormatter.onRejected(error).catch(e =>
182 | expect(e).toMatchInlineSnapshot(`
183 | Object {
184 | "error_message": "MOCK_MESSAGE",
185 | "error_type": "MOCK_ERROR",
186 | "extra": Object {
187 | "foo": "bar",
188 | },
189 | "status_code": 0,
190 | }
191 | `),
192 | )
193 | })
194 | })
195 |
196 | describe('royally fucked up cases', () => {
197 | const royallyFuckedUpError = new Error('undefined is not a function')
198 |
199 | responseFormatter
200 | .onRejected(royallyFuckedUpError as any)
201 | .catch(e => expect(e).toBe(royallyFuckedUpError))
202 | })
203 | })
204 |
--------------------------------------------------------------------------------
/src/interceptors/response/responseFormatter.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import {
3 | IPutioAPIClientError,
4 | IPutioAPIClientErrorData,
5 | PutioAPIClientResponseInterceptorFactory,
6 | } from '../../client/types'
7 | import { isPutioAPIErrorResponse } from '../../utils'
8 |
9 | export const createResponseFormatter: PutioAPIClientResponseInterceptorFactory = () => ({
10 | onFulfilled: response => ({
11 | ...response,
12 | body: response.data,
13 | }),
14 |
15 | onRejected: (error: unknown) => {
16 | if (!axios.isAxiosError(error)) {
17 | return Promise.reject(error)
18 | }
19 |
20 | try {
21 | let errorData: IPutioAPIClientErrorData = {
22 | 'x-trace-id': error.response?.headers['x-trace-id'],
23 | error_message: error.message,
24 | error_type: 'ERROR',
25 | status_code: 0,
26 | extra: {},
27 | }
28 |
29 | // ECONNABORTED is the code for a request that timed out in axios.
30 | if (error.code === 'ECONNABORTED') {
31 | errorData = {
32 | ...errorData,
33 | status_code: 408,
34 | error_message: 'Request timed out',
35 | }
36 | }
37 |
38 | if (error.response && error.response.data) {
39 | const { status, data } = error.response
40 | errorData = isPutioAPIErrorResponse(data)
41 | ? {
42 | ...errorData,
43 | ...data,
44 | status_code: status,
45 | }
46 | : {
47 | ...errorData,
48 | status_code: status,
49 | }
50 | } else if (
51 | error.request instanceof XMLHttpRequest &&
52 | error.request.readyState === 4
53 | ) {
54 | const { status, responseText } = error.request
55 | const data = JSON.parse(responseText)
56 |
57 | errorData = {
58 | ...errorData,
59 | ...data,
60 | status_code: status,
61 | }
62 | }
63 |
64 | const formattedError: IPutioAPIClientError = {
65 | ...error,
66 | data: errorData,
67 | toJSON: () => errorData,
68 | }
69 |
70 | return Promise.reject(formattedError)
71 | } catch (e) {
72 | return Promise.reject(error)
73 | }
74 | },
75 | })
76 |
--------------------------------------------------------------------------------
/src/resources/Account/Account.ts:
--------------------------------------------------------------------------------
1 | import { PutioAPIClient } from '../../client'
2 | import {
3 | IAccountInfo,
4 | AccountInfoParams,
5 | IAccountSettings,
6 | SaveAccountSettingsPayload,
7 | IAccountConfirmation,
8 | AccountClearOptions,
9 | } from './types'
10 |
11 | export default class Account {
12 | private client: PutioAPIClient
13 |
14 | constructor(client: PutioAPIClient) {
15 | this.client = client
16 | }
17 |
18 | public Info(params: AccountInfoParams = {}) {
19 | return this.client.get<{ info: IAccountInfo }>('/account/info', {
20 | params,
21 | })
22 | }
23 |
24 | public Settings() {
25 | return this.client.get<{ settings: IAccountSettings }>('/account/settings')
26 | }
27 |
28 | public SaveSettings(payload: SaveAccountSettingsPayload) {
29 | return this.client.post('/account/settings', {
30 | data: payload,
31 | })
32 | }
33 |
34 | public Clear(options: AccountClearOptions) {
35 | return this.client.post<{}>('/account/clear', {
36 | data: options,
37 | })
38 | }
39 |
40 | public Destroy(currentPassword: string) {
41 | return this.client.post<{}>('/account/destroy', {
42 | data: {
43 | current_password: currentPassword,
44 | },
45 | })
46 | }
47 |
48 | public Confirmations(type?: IAccountConfirmation['subject']) {
49 | return this.client.get<{ confirmations: IAccountConfirmation[] }>(
50 | '/account/confirmation/list',
51 | {
52 | data: {
53 | type,
54 | },
55 | },
56 | )
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/resources/Account/types.ts:
--------------------------------------------------------------------------------
1 | export interface IAccountSettings {
2 | beta_user: boolean
3 | callback_url: string
4 | dark_theme: boolean
5 | default_download_folder: number
6 | disable_subtitles: boolean
7 | dont_autoselect_subtitles: boolean
8 | fluid_layout: boolean
9 | hide_subtitles: boolean
10 | history_enabled: boolean
11 | is_invisible: boolean
12 | locale: string
13 | login_mails_enabled: boolean
14 | next_episode: boolean
15 | pushover_token: string | null
16 | show_optimistic_usage: boolean
17 | sort_by: string
18 | start_from: boolean
19 | subtitle_languages: [string | null, string | null]
20 | theater_mode: boolean
21 | theme: 'dark' | 'light' | 'auto'
22 | transfer_sort_by: string
23 | trash_enabled: boolean
24 | tunnel_route_name: null | string
25 | two_factor_enabled: boolean
26 | use_private_download_ip: boolean
27 | use_start_from: boolean
28 | video_player: 'html5' | 'flash' | null
29 | }
30 |
31 | export type SaveAccountSettingsPayload =
32 | | Partial
33 | | { username: string }
34 | | {
35 | mail: string
36 | current_password: string
37 | }
38 | | {
39 | password: string
40 | current_password: string
41 | }
42 | | {
43 | two_factor_enabled: {
44 | enable: boolean
45 | code: string
46 | }
47 | }
48 |
49 | export interface IAccountInfo {
50 | account_status: 'active' | 'inactive' | 'stranger'
51 | avatar_url: string
52 | can_create_sub_account: boolean
53 | disk: {
54 | avail: number
55 | used: number
56 | size: number
57 | }
58 | download_token?: string
59 | family_owner?: string
60 | features: Record
61 | files_will_be_deleted_at: string | null
62 | is_admin: boolean
63 | is_eligible_for_friend_invitation: boolean
64 | is_sub_account: boolean
65 | mail: string
66 | monthly_bandwidth_usage: number
67 | pas?: { user_hash: string }
68 | push_token?: string
69 | password_last_changed_at: string | null
70 | private_download_host_ip: string | null
71 | settings: IAccountSettings
72 | trash_size: number
73 | user_hash: string
74 | user_id: number
75 | username: string
76 | warnings: {
77 | callback_url_has_failed?: boolean
78 | pushover_token_has_failed?: boolean
79 | }
80 | }
81 |
82 | export interface IAccountConfirmation {
83 | subject: 'mail_change' | 'password_change' | 'subscription_upgrade'
84 | created_at: string
85 | }
86 |
87 | export const ACCOUNT_CLEAR_OPTION_KEYS = [
88 | 'files',
89 | 'finished_transfers',
90 | 'active_transfers',
91 | 'rss_feeds',
92 | 'rss_logs',
93 | 'history',
94 | 'trash',
95 | 'friends',
96 | ] as const
97 |
98 | export type AccountInfoParams = {
99 | download_token?: 0 | 1
100 | push_token?: 0 | 1
101 | sharing?: 0 | 1
102 | features?: 0 | 1
103 | intercom?: 0 | 1
104 | pas?: 0 | 1
105 | profitwell?: 0 | 1
106 | platform?: string
107 | }
108 |
109 | export type AccountClearOptions = Record<
110 | typeof ACCOUNT_CLEAR_OPTION_KEYS[number],
111 | boolean
112 | >
113 |
--------------------------------------------------------------------------------
/src/resources/Auth/Auth.ts:
--------------------------------------------------------------------------------
1 | import { Base64 } from 'js-base64'
2 | import URI from 'urijs'
3 | import { PutioAPIClient } from '../../client'
4 | import {
5 | IGenerateTOTPResponse,
6 | ILoginResponse,
7 | IVerifyTOTPResponse,
8 | IValidateTokenResponse,
9 | OAuthApp,
10 | OAuthAppSession,
11 | TwoFactorRecoveryCodes,
12 | } from './types'
13 |
14 | class TwoFactor {
15 | private client: PutioAPIClient
16 |
17 | constructor(client: PutioAPIClient) {
18 | this.client = client
19 | }
20 |
21 | public GenerateTOTP() {
22 | return this.client.post('/two_factor/generate/totp')
23 | }
24 |
25 | public VerifyTOTP(twoFactorScopedToken: string, code: string) {
26 | return this.client.post('/two_factor/verify/totp', {
27 | params: { oauth_token: twoFactorScopedToken },
28 | data: { code },
29 | })
30 | }
31 |
32 | public GetRecoveryCodes() {
33 | return this.client.get<{ recovery_codes: TwoFactorRecoveryCodes }>(
34 | '/two_factor/recovery_codes',
35 | )
36 | }
37 |
38 | public RegenerateRecoveryCodes() {
39 | return this.client.post<{ recovery_codes: TwoFactorRecoveryCodes }>(
40 | '/two_factor/recovery_codes/refresh',
41 | )
42 | }
43 | }
44 |
45 | export default class Auth {
46 | private client: PutioAPIClient
47 |
48 | public TwoFactor: TwoFactor
49 |
50 | constructor(client: PutioAPIClient) {
51 | this.client = client
52 | this.TwoFactor = new TwoFactor(client)
53 | }
54 |
55 | public GetLoginURL({
56 | redirectURI,
57 | responseType = 'token',
58 | state,
59 | clientID,
60 | clientName,
61 | }: {
62 | redirectURI: string
63 | responseType: string
64 | state: string
65 | clientID: string
66 | clientName?: string
67 | }): string {
68 | const { options } = this.client
69 |
70 | const url = new URI(`${options.webAppURL}/authenticate`).addQuery({
71 | client_id: clientID || options.clientID,
72 | client_name: clientName,
73 | redirect_uri: redirectURI,
74 | response_type: responseType,
75 | isolated: 1,
76 | state,
77 | })
78 |
79 | return url.toString()
80 | }
81 |
82 | public Login({
83 | username,
84 | password,
85 | app,
86 | }: {
87 | username: string
88 | password: string
89 | app: {
90 | client_id: string
91 | client_secret: string
92 | }
93 | }) {
94 | return this.client.put(
95 | `/oauth2/authorizations/clients/${app.client_id}?client_secret=${app.client_secret}`,
96 | {
97 | headers: {
98 | Authorization: `Basic ${Base64.encode(`${username}:${password}`)}`,
99 | },
100 | },
101 | )
102 | }
103 |
104 | public Logout() {
105 | return this.client.post('/oauth/grants/logout')
106 | }
107 |
108 | public Register(data: any) {
109 | return this.client.post('/registration/register', {
110 | data: {
111 | client_id: this.client.options.clientID,
112 | ...data,
113 | },
114 | })
115 | }
116 |
117 | public Exists(key: string, value: string) {
118 | return this.client.get(`/registration/exists/${key}`, {
119 | params: { value },
120 | })
121 | }
122 |
123 | public GetVoucher(code: string) {
124 | return this.client.get(`/registration/voucher/${code}`)
125 | }
126 |
127 | public GetGiftCard(code: string) {
128 | return this.client.get(`/registration/gift_card/${code}`)
129 | }
130 |
131 | public GetFamilyInvite(code: string) {
132 | return this.client.get(`/registration/family/${code}`)
133 | }
134 |
135 | public ForgotPassword(mail: string) {
136 | return this.client.post('/registration/password/forgot', {
137 | data: { mail },
138 | })
139 | }
140 |
141 | public ResetPassword(key: string, newPassword: string) {
142 | return this.client.post('/registration/password/reset', {
143 | data: {
144 | key,
145 | password: newPassword,
146 | },
147 | })
148 | }
149 |
150 | public GetCode(clientID: number | string, clientName?: string) {
151 | return this.client.get<{ code: string; qr_code_url: string }>(
152 | '/oauth2/oob/code',
153 | {
154 | params: { app_id: clientID, client_name: clientName },
155 | },
156 | )
157 | }
158 |
159 | public CheckCodeMatch(code: string) {
160 | return this.client.get<{ oauth_token: string | null }>(
161 | `/oauth2/oob/code/${code}`,
162 | )
163 | }
164 |
165 | public LinkDevice(code: string) {
166 | return this.client.post<{ app: OAuthApp }>('/oauth2/oob/code', {
167 | data: { code },
168 | })
169 | }
170 |
171 | public Grants() {
172 | return this.client.get<{ apps: OAuthApp[] }>('/oauth/grants/')
173 | }
174 |
175 | public RevokeApp(id: number) {
176 | return this.client.post<{}>(`/oauth/grants/${id}/delete`)
177 | }
178 |
179 | public Clients() {
180 | return this.client.get<{ clients: OAuthAppSession[] }>('/oauth/clients/')
181 | }
182 |
183 | public RevokeClient(id: string) {
184 | return this.client.post<{}>(`/oauth/clients/${id}/delete`)
185 | }
186 |
187 | public RevokeAllClients() {
188 | return this.client.post<{}>('/oauth/clients/delete-all')
189 | }
190 |
191 | public ValidateToken(token: string) {
192 | return this.client.get('/oauth2/validate', {
193 | params: { oauth_token: token },
194 | })
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/src/resources/Auth/OAuth.ts:
--------------------------------------------------------------------------------
1 | import URI from 'urijs'
2 | import { createFormDataFromObject } from '../../utils'
3 | import { PutioAPIClient } from '../../client'
4 | import { MyOAuthApp, PopularOAuthApp } from './types'
5 |
6 | export default class OAuth {
7 | private client: PutioAPIClient
8 |
9 | constructor(client: PutioAPIClient) {
10 | this.client = client
11 | }
12 |
13 | public GetAuthorizeURL(query: object = {}): string {
14 | const {
15 | token,
16 | options: { baseURL },
17 | } = this.client
18 |
19 | const uri = new URI(`${baseURL}/oauth2/authorize`).addQuery({
20 | ...query,
21 | oauth_token: token,
22 | })
23 |
24 | return uri.toString()
25 | }
26 |
27 | public Query() {
28 | return this.client.get<{ apps: MyOAuthApp[] }>('/oauth/apps')
29 | }
30 |
31 | public Get(id: MyOAuthApp['id']) {
32 | return this.client.get<{ app: MyOAuthApp; token: string }>(
33 | `/oauth/apps/${id}`,
34 | )
35 | }
36 |
37 | public GetIconURL(id: MyOAuthApp['id']): string {
38 | const {
39 | token,
40 | options: { baseURL },
41 | } = this.client
42 |
43 | return `${baseURL}/oauth/apps/${id}/icon?oauth_token=${token}`
44 | }
45 |
46 | public SetIcon(id: MyOAuthApp['id'], data: object) {
47 | return this.client.post(`/oauth/apps/${id}/icon`, { data })
48 | }
49 |
50 | public Create(app: Omit) {
51 | return this.client.post<{ app: MyOAuthApp }>('/oauth/apps/register', {
52 | data: createFormDataFromObject(app),
53 | })
54 | }
55 |
56 | public Update(app: MyOAuthApp) {
57 | return this.client.post<{ app: MyOAuthApp }>(`/oauth/apps/${app.id}`, {
58 | data: createFormDataFromObject(app),
59 | })
60 | }
61 |
62 | public Delete(id: MyOAuthApp['id']) {
63 | return this.client.post(`/oauth/apps/${id}/delete`)
64 | }
65 |
66 | public RegenerateToken(id: MyOAuthApp['id']) {
67 | return this.client.post<{ access_token: string }>(
68 | `/oauth/apps/${id}/regenerate_token`,
69 | )
70 | }
71 |
72 | public GetPopularApps() {
73 | return this.client.get<{ apps: PopularOAuthApp[] }>('/oauth/apps/popular')
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/resources/Auth/types.ts:
--------------------------------------------------------------------------------
1 | import { ISODateString } from 'utils/types'
2 |
3 | export interface ILoginResponse {
4 | access_token: string
5 | user_id: number
6 | }
7 |
8 | export interface IValidateTokenResponse {
9 | result: boolean
10 | token_id: number
11 | token_scope: 'default' | 'two_factor'
12 | user_id: number
13 | }
14 |
15 | export type TwoFactorRecoveryCodes = {
16 | created_at: ISODateString
17 | codes: Array<{
18 | code: string
19 | used_at: ISODateString | null
20 | }>
21 | }
22 |
23 | export interface IGenerateTOTPResponse {
24 | secret: string
25 | uri: string
26 | recovery_codes: TwoFactorRecoveryCodes
27 | }
28 |
29 | export interface IVerifyTOTPResponse {
30 | token: string
31 | user_id: number
32 | }
33 |
34 | export type OAuthApp = {
35 | id: number
36 | name: string
37 | description: string
38 | website: string
39 | has_icon: boolean
40 | }
41 |
42 | export type MyOAuthApp = OAuthApp & {
43 | users: number
44 | callback: string
45 | secret: string
46 | hidden: boolean
47 | }
48 |
49 | export type PopularOAuthApp = OAuthApp & {
50 | maker: string
51 | /** admin only */
52 | hidden?: boolean
53 | /** admin only */
54 | users?: number
55 | }
56 |
57 | export type OAuthAppSession = {
58 | id: number
59 | app_id: OAuthApp['id']
60 | app_name: OAuthApp['name']
61 | active: boolean
62 | client_name: string
63 | ip_address: string
64 | user_agent: string
65 | note: string | null
66 | created_at: string
67 | last_used_at: string
68 | }
69 |
--------------------------------------------------------------------------------
/src/resources/Config.ts:
--------------------------------------------------------------------------------
1 | import { PutioAPIClient } from '../client'
2 |
3 | export default class Config {
4 | private client: PutioAPIClient
5 |
6 | constructor(client: PutioAPIClient) {
7 | this.client = client
8 | }
9 |
10 | public Read() {
11 | return this.client.get<{ config: Config }>('/config')
12 | }
13 |
14 | public Write(config: Config) {
15 | return this.client.put<{}>('/config', {
16 | data: { config },
17 | })
18 | }
19 |
20 | public GetKey(key: Key) {
21 | return this.client.get<{ value: Config[Key] }>(`/config/${key}`)
22 | }
23 |
24 | public SetKey(
25 | key: Key,
26 | value: Config[Key],
27 | ) {
28 | return this.client.put<{}>(`/config/${key}`, {
29 | data: { value },
30 | })
31 | }
32 |
33 | public DeleteKey(key: Key) {
34 | return this.client.delete<{}>(`/config/${key}`)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/resources/DownloadLinks/DownloadLinks.ts:
--------------------------------------------------------------------------------
1 | import { PutioAPIClient } from '../../client'
2 | import {
3 | IDownloadLinksCreateResponse,
4 | IDownloadLinksInfoResponse,
5 | } from './types'
6 |
7 | export default class DownloadLinks {
8 | private client: PutioAPIClient
9 |
10 | constructor(client: PutioAPIClient) {
11 | this.client = client
12 | }
13 |
14 | public Create({
15 | ids = [],
16 | cursor,
17 | excludeIds = [],
18 | }: {
19 | ids?: number[]
20 | cursor?: string
21 | excludeIds?: number[]
22 | }) {
23 | return this.client.post(
24 | '/download_links/create',
25 | {
26 | data: {
27 | file_ids: ids.join(','),
28 | exclude_ids: excludeIds.join(','),
29 | cursor,
30 | },
31 | },
32 | )
33 | }
34 |
35 | public Get(downloadLinksId: number) {
36 | return this.client.get(
37 | `/download_links/${downloadLinksId}`,
38 | )
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/resources/DownloadLinks/types.ts:
--------------------------------------------------------------------------------
1 | export interface IDownloadLinksCreateResponse {
2 | id: number
3 | }
4 |
5 | export interface IDownloadLinksInfoResponse {
6 | links: IDownloadLinks
7 | links_status: DownloadLinksStatuses
8 | }
9 |
10 | interface IDownloadLinks {
11 | download_links: string[]
12 | mp4_links: string[]
13 | media_links: string[]
14 | }
15 |
16 | export type DownloadLinksStatuses = 'NEW' | 'PROCESSING' | 'DONE' | 'ERROR'
17 |
--------------------------------------------------------------------------------
/src/resources/Events/Events.ts:
--------------------------------------------------------------------------------
1 | import { PutioAPIClient } from '../../client'
2 | import {
3 | IHistoryClearAllEventsResponse,
4 | IHistoryDeleteEventResponse,
5 | IHistoryResponse,
6 | } from './types'
7 |
8 | export default class PutioEvents {
9 | private client: PutioAPIClient
10 |
11 | constructor(client: PutioAPIClient) {
12 | this.client = client
13 | }
14 |
15 | public Query() {
16 | return this.client.get('/events/list')
17 | }
18 |
19 | public Delete(id: number) {
20 | return this.client.post(`/events/delete/${id}`)
21 | }
22 |
23 | public Clear() {
24 | return this.client.post('/events/delete')
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/resources/Events/types.ts:
--------------------------------------------------------------------------------
1 | interface IHistoryBaseEvent {
2 | id: number
3 | created_at: string
4 | user_id: number
5 | }
6 |
7 | interface IHistoryFileEvent extends IHistoryBaseEvent {
8 | file_id: number
9 | file_name: string
10 | file_size: number
11 | }
12 |
13 | export interface IHistoryFileSharedEvent extends IHistoryFileEvent {
14 | type: 'file_shared'
15 | sharing_user_name: string
16 | }
17 |
18 | export interface IHistoryUploadEvent extends IHistoryFileEvent {
19 | type: 'upload'
20 | }
21 |
22 | export interface IHistoryFileFromRSSDeletedEvent extends IHistoryFileEvent {
23 | type: 'file_from_rss_deleted_for_space'
24 | file_source: string
25 | }
26 |
27 | interface IHistoryTransferEvent extends IHistoryBaseEvent {
28 | source: string
29 | transfer_name: string
30 | }
31 |
32 | export interface IHistoryTransferCompletedEvent extends IHistoryTransferEvent {
33 | type: 'transfer_completed'
34 | file_id: number
35 | transfer_size: number
36 | }
37 |
38 | export interface IHistoryTransferErrorEvent extends IHistoryTransferEvent {
39 | type: 'transfer_error'
40 | }
41 |
42 | export interface IHistoryTransferCallbackErrorEvent extends IHistoryBaseEvent {
43 | type: 'transfer_callback_error'
44 | transfer_id: number
45 | transfer_name: string
46 | message: string
47 | }
48 |
49 | export interface IHistoryPrivateTorrentPinEvent extends IHistoryBaseEvent {
50 | type: 'private_torrent_pin'
51 | user_download_name: string
52 | pinned_host_ip: string
53 | new_host_ip: string
54 | }
55 |
56 | export interface IHistoryRSSFilterPausedEvent extends IHistoryBaseEvent {
57 | type: 'rss_filter_paused'
58 | rss_filter_id: number
59 | rss_filter_title: string
60 | }
61 |
62 | export interface IHistoryZipCreatedEvent extends IHistoryBaseEvent {
63 | type: 'zip_created'
64 | zip_id: number
65 | zip_size: number
66 | }
67 |
68 | export interface IHistoryVoucherEvent extends IHistoryBaseEvent {
69 | type: 'voucher'
70 | voucher: number
71 | voucher_owner_id: number
72 | voucher_owner_name: string
73 | }
74 |
75 | export interface IHistoryTransferFromRSSErrorEvent extends IHistoryBaseEvent {
76 | type: 'transfer_from_rss_error'
77 | rss_id: number
78 | transfer_name: string
79 | }
80 |
81 | export type IHistoryEvent =
82 | | IHistoryFileSharedEvent
83 | | IHistoryUploadEvent
84 | | IHistoryFileFromRSSDeletedEvent
85 | | IHistoryTransferCompletedEvent
86 | | IHistoryTransferErrorEvent
87 | | IHistoryTransferFromRSSErrorEvent
88 | | IHistoryTransferCallbackErrorEvent
89 | | IHistoryPrivateTorrentPinEvent
90 | | IHistoryRSSFilterPausedEvent
91 | | IHistoryVoucherEvent
92 | | IHistoryZipCreatedEvent
93 |
94 | export interface IHistoryResponse {
95 | events: IHistoryEvent[]
96 | }
97 |
98 | export interface IHistoryDeleteEventResponse {
99 | success: 'OK'
100 | }
101 |
102 | export interface IHistoryClearAllEventsResponse {
103 | success: 'OK'
104 | }
105 |
--------------------------------------------------------------------------------
/src/resources/Files/File.spec.ts:
--------------------------------------------------------------------------------
1 | import PutioAPIClient from '../../index'
2 |
3 | describe('resources/Files/File', () => {
4 | const API = new PutioAPIClient({
5 | baseURL: '',
6 | })
7 |
8 | it('should create correct HLS stream URL when no params given', () => {
9 | expect(API.File.GetHLSStreamURL(0)).toBe('/files/0/hls/media.m3u8')
10 | })
11 |
12 | it('should add token to HLS stream url if set', () => {
13 | API.setToken('token')
14 |
15 | expect(API.File.GetHLSStreamURL(0)).toBe(
16 | '/files/0/hls/media.m3u8?oauth_token=token',
17 | )
18 |
19 | API.clearToken()
20 | })
21 |
22 | it('should create correct HLS stream URL when params given', () => {
23 | expect(API.File.GetHLSStreamURL(0, { playOriginal: true })).toBe(
24 | '/files/0/hls/media.m3u8?original=1',
25 | )
26 |
27 | expect(API.File.GetHLSStreamURL(0, { maxSubtitleCount: 1 })).toBe(
28 | '/files/0/hls/media.m3u8?max_subtitle_count=1',
29 | )
30 |
31 | expect(
32 | API.File.GetHLSStreamURL(0, { playOriginal: true, maxSubtitleCount: 1 }),
33 | ).toBe('/files/0/hls/media.m3u8?max_subtitle_count=1&original=1')
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/src/resources/Files/File.ts:
--------------------------------------------------------------------------------
1 | import URI from 'urijs'
2 | import { PutioAPIClient } from '../../client'
3 | import { FileConversionStatus } from './types'
4 | export default class File {
5 | private client: PutioAPIClient
6 |
7 | constructor(client: PutioAPIClient) {
8 | this.client = client
9 | }
10 |
11 | public Download(fileId: number) {
12 | return this.client.get(`/files/${fileId}/download`)
13 | }
14 |
15 | public GetStorageURL(fileId: number) {
16 | return this.client.get(`/files/${fileId}/url`)
17 | }
18 |
19 | public GetContent(fileId: number) {
20 | return this.client.get(`/files/${fileId}/stream`)
21 | }
22 |
23 | public Subtitles(fileId: number, oauthToken: string, languages: string[]) {
24 | return this.client.get(`/files/${fileId}/subtitles`, {
25 | params: {
26 | languages,
27 | oauth_token: oauthToken,
28 | },
29 | })
30 | }
31 |
32 | public GetHLSStreamURL(
33 | fileId: number,
34 | {
35 | token = '',
36 | subtitleLanguages = [],
37 | maxSubtitleCount,
38 | playOriginal,
39 | }: {
40 | token?: string
41 | subtitleLanguages?: string[]
42 | maxSubtitleCount?: number
43 | playOriginal?: boolean
44 | } = {},
45 | ) {
46 | return new URI(
47 | `${this.client.options.baseURL}/files/${fileId}/hls/media.m3u8`,
48 | )
49 | .addQuery({
50 | oauth_token: token || this.client.token,
51 | subtitle_languages: subtitleLanguages,
52 | max_subtitle_count: maxSubtitleCount,
53 | original:
54 | typeof playOriginal === 'boolean'
55 | ? playOriginal
56 | ? 1
57 | : 0
58 | : undefined,
59 | })
60 | .toString()
61 | }
62 |
63 | public ConvertToMp4(fileId: number) {
64 | return this.client.post<{ mp4: FileConversionStatus }>(
65 | `/files/${fileId}/mp4`,
66 | )
67 | }
68 |
69 | public ConvertStatus(fileId: number) {
70 | return this.client.get<{ mp4: FileConversionStatus }>(
71 | `/files/${fileId}/mp4`,
72 | )
73 | }
74 |
75 | public DeleteMp4(fileId: number) {
76 | return this.client.delete<{}>(`/files/${fileId}/mp4`)
77 | }
78 |
79 | public SharedWith(fileId: number) {
80 | return this.client.get(`/files/${fileId}/shared-with`)
81 | }
82 |
83 | public Unshare(fileId: number, shareId: any) {
84 | let shares = shareId
85 |
86 | if (shares) {
87 | shares = Array.isArray(shares)
88 | ? shares.map(i => i.toString())
89 | : [shares.toString()]
90 | shares = shares.join(',')
91 | }
92 |
93 | return this.client.post(`/files/${fileId}/unshare`, {
94 | data: {
95 | shares: shares || 'everyone',
96 | },
97 | })
98 | }
99 |
100 | public SaveAsMp4(fileId: number) {
101 | return this.client.get(`/files/${fileId}/put-mp4-to-my-folders`)
102 | }
103 |
104 | public Rename(fileId: number, name: string) {
105 | return this.client.post('/files/rename', {
106 | data: {
107 | file_id: fileId,
108 | name,
109 | },
110 | })
111 | }
112 |
113 | public GetStartFrom(fileId: number) {
114 | return this.client.get<{ start_from: number }>(
115 | `/files/${fileId}/start-from`,
116 | )
117 | }
118 |
119 | public SetStartFrom(fileId: number, time: string) {
120 | return this.client.post(`/files/${fileId}/start-from/set`, {
121 | data: {
122 | time: parseInt(time, 10),
123 | },
124 | })
125 | }
126 |
127 | public ResetStartFrom(fileId: number) {
128 | return this.client.get(`/files/${fileId}/start-from/delete`)
129 | }
130 |
131 | public Extract(fileId: number, password?: string) {
132 | return this.client.post('/files/extract', {
133 | data: {
134 | password,
135 | user_file_ids: [fileId.toString()],
136 | },
137 | })
138 | }
139 |
140 | public FindNextFile(fileId: number, fileType: string) {
141 | return this.client.get(`/files/${fileId}/next-file`, {
142 | params: { file_type: fileType },
143 | })
144 | }
145 |
146 | public FindNextVideo(fileId: number) {
147 | return this.client.get(`/files/${fileId}/next-video`)
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/resources/Files/Files.spec.ts:
--------------------------------------------------------------------------------
1 | import PutioAPIClient, { isPutioAPIError } from '../../index'
2 |
3 | describe('resources/Files/Files', () => {
4 | const API = new PutioAPIClient({})
5 |
6 | it('should construct correct payload for Upload method', async () => {
7 | try {
8 | API.setToken('test-token')
9 |
10 | await API.Files.Upload({
11 | file: new File([], 'test'),
12 | parentId: 0,
13 | })
14 | } catch (e) {
15 | if (isPutioAPIError(e)) {
16 | expect(e.config.headers['Authorization']).toBe(`token test-token`)
17 | }
18 | }
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/src/resources/Files/Files.ts:
--------------------------------------------------------------------------------
1 | import { PutioAPIClient } from '../../client'
2 | import {
3 | FileSortOption,
4 | FileType,
5 | IFileDeleteResponse,
6 | ISearchResponse,
7 | } from './types'
8 |
9 | export default class Files {
10 | private client: PutioAPIClient
11 |
12 | constructor(client: PutioAPIClient) {
13 | this.client = client
14 | }
15 |
16 | public Query(
17 | id: number | string,
18 | {
19 | perPage,
20 | sortBy,
21 | contentType,
22 | fileType,
23 | streamUrl,
24 | streamUrlParent,
25 | mp4StreamUrl,
26 | mp4StreamUrlParent,
27 | hidden,
28 | mp4Status,
29 | mp4StatusParent,
30 | videoMetadata,
31 | videoMetadataParent,
32 | codecsParent,
33 | mediaInfoParent,
34 | breadcrumbs,
35 | total,
36 | }: {
37 | perPage?: number
38 | sortBy?: FileSortOption
39 | contentType?: string
40 | fileType?: FileType
41 | streamUrl?: boolean
42 | streamUrlParent?: boolean
43 | mp4StreamUrl?: boolean
44 | mp4StreamUrlParent?: boolean
45 | hidden?: boolean
46 | mp4Status?: boolean
47 | mp4StatusParent?: boolean
48 | videoMetadata?: boolean
49 | videoMetadataParent?: boolean
50 | codecsParent?: boolean
51 | mediaInfoParent?: boolean
52 | breadcrumbs?: boolean
53 | total?: boolean
54 | } = {},
55 | ) {
56 | return this.client.get(
57 | `/files/${id === 'friends' ? 'items-shared-with-you' : 'list'}`,
58 | {
59 | params: {
60 | parent_id: id !== 'friends' ? id : null,
61 | per_page: perPage,
62 | sort_by: sortBy,
63 | content_type: contentType,
64 | file_type: fileType,
65 | stream_url: streamUrl,
66 | stream_url_parent: streamUrlParent,
67 | mp4_stream_url: mp4StreamUrl,
68 | mp4_stream_url_parent: mp4StreamUrlParent,
69 | hidden,
70 | mp4_status: mp4Status,
71 | mp4_status_parent: mp4StatusParent,
72 | video_metadata: videoMetadata,
73 | video_metadata_parent: videoMetadataParent,
74 | codecs_parent: codecsParent,
75 | media_info_parent: mediaInfoParent,
76 | breadcrumbs,
77 | total,
78 | },
79 | },
80 | )
81 | }
82 |
83 | public Continue(cursor: string, { perPage }: { perPage?: number } = {}) {
84 | return this.client.post('/files/list/continue', {
85 | data: {
86 | cursor,
87 | },
88 | params: {
89 | per_page: perPage,
90 | },
91 | })
92 | }
93 |
94 | public Search(
95 | query: string,
96 | {
97 | perPage,
98 | fileType,
99 | }: { perPage: number; fileType?: FileType | FileType[] } = { perPage: 50 },
100 | ) {
101 | return this.client.get('/files/search', {
102 | params: {
103 | query,
104 | per_page: perPage,
105 | type: fileType,
106 | },
107 | })
108 | }
109 |
110 | public ContinueSearch(
111 | cursor: string,
112 | { perPage }: { perPage?: number } = {},
113 | ) {
114 | return this.client.post('/files/search/continue', {
115 | data: {
116 | cursor,
117 | },
118 | params: {
119 | per_page: perPage,
120 | },
121 | })
122 | }
123 |
124 | public NewFolder(name: string, parentId: number = 0) {
125 | return this.CreateFolder({ name, parentId })
126 | }
127 |
128 | public CreateFolder({
129 | name,
130 | parentId,
131 | path,
132 | }: {
133 | name: string
134 | parentId?: number
135 | path?: string
136 | }) {
137 | return this.client.post('/files/create-folder', {
138 | data: {
139 | name,
140 | parent_id: parentId,
141 | path,
142 | },
143 | })
144 | }
145 |
146 | public DeleteAll(
147 | cursor: string,
148 | excludeIds: number[] = [],
149 | {
150 | partialDelete = false,
151 | skipTrash,
152 | }: {
153 | partialDelete?: boolean
154 | skipTrash?: boolean
155 | },
156 | ) {
157 | return this.client.post('/files/delete', {
158 | data: {
159 | cursor,
160 | exclude_ids: excludeIds.join(','),
161 | },
162 | params: {
163 | skip_nonexistents: true,
164 | partial_delete: partialDelete,
165 | skip_trash: skipTrash,
166 | },
167 | })
168 | }
169 |
170 | public Delete(
171 | ids: number[] = [],
172 | {
173 | ignoreFileOwner = false,
174 | partialDelete = false,
175 | skipTrash,
176 | }: {
177 | ignoreFileOwner?: boolean
178 | partialDelete?: boolean
179 | skipTrash?: boolean
180 | } = {},
181 | ) {
182 | return this.client.post('/files/delete', {
183 | data: {
184 | file_ids: ids.join(','),
185 | },
186 | params: {
187 | skip_nonexistents: true,
188 | skip_owner_check: ignoreFileOwner,
189 | partial_delete: partialDelete,
190 | skip_trash: skipTrash,
191 | },
192 | })
193 | }
194 |
195 | public Extract({
196 | ids = [],
197 | cursor,
198 | excludeIds = [],
199 | }: {
200 | ids?: number[]
201 | cursor?: string
202 | excludeIds?: number[]
203 | }) {
204 | return this.client.post('/files/extract', {
205 | data: {
206 | user_file_ids: ids.join(','),
207 | exclude_ids: excludeIds.join(','),
208 | cursor,
209 | },
210 | })
211 | }
212 |
213 | public ExtractStatus() {
214 | return this.client.get('/files/extract')
215 | }
216 |
217 | public Share({
218 | ids = [],
219 | cursor,
220 | excludeIds = [],
221 | friends,
222 | }: {
223 | ids?: number[]
224 | cursor?: string
225 | excludeIds?: number[]
226 | friends: any
227 | }) {
228 | return this.client.post('/files/share', {
229 | data: {
230 | cursor,
231 | friends,
232 | file_ids: ids.join(','),
233 | exclude_ids: excludeIds.join(','),
234 | },
235 | })
236 | }
237 |
238 | public Move(ids: number[], to: number) {
239 | return this.client.post('/files/move', {
240 | data: {
241 | file_ids: ids.join(','),
242 | parent_id: to,
243 | },
244 | })
245 | }
246 |
247 | public MoveAll({
248 | cursor,
249 | excludeIds = [],
250 | to,
251 | }: {
252 | cursor?: string
253 | excludeIds?: number[]
254 | to: number
255 | }) {
256 | return this.client.post('/files/move', {
257 | data: {
258 | cursor,
259 | parent_id: to,
260 | exclude_ids: excludeIds.join(','),
261 | },
262 | })
263 | }
264 |
265 | public ConvertToMp4({
266 | ids = [],
267 | cursor,
268 | excludeIds = [],
269 | }: {
270 | ids?: number[]
271 | cursor?: string
272 | excludeIds?: number[]
273 | }) {
274 | return this.client.post('/files/convert_mp4', {
275 | data: {
276 | file_ids: ids.join(','),
277 | exclude_ids: excludeIds.join(','),
278 | cursor,
279 | },
280 | })
281 | }
282 |
283 | public SharedOnes() {
284 | return this.client.get('/files/shared')
285 | }
286 |
287 | public PublicShares() {
288 | return this.client.get('/files/public/list')
289 | }
290 |
291 | public SetWatchStatus({
292 | ids = [],
293 | cursor,
294 | excludeIds = [],
295 | watched,
296 | }: {
297 | ids?: number[]
298 | cursor?: string
299 | excludeIds?: number[]
300 | watched: boolean
301 | }) {
302 | return this.client.post('/files/watch-status', {
303 | data: {
304 | file_ids: ids.join(','),
305 | exclude_ids: excludeIds.join(','),
306 | cursor,
307 | watched,
308 | },
309 | })
310 | }
311 |
312 | public Upload({
313 | file,
314 | fileName,
315 | parentId = 0,
316 | }: {
317 | file: any
318 | fileName?: string
319 | parentId?: number
320 | }) {
321 | const form = new FormData()
322 | form.append('file', file)
323 |
324 | if (fileName) {
325 | form.append('filename', fileName)
326 | }
327 |
328 | if (parentId) {
329 | form.append('parent_id', parentId.toString())
330 | }
331 |
332 | return this.client.post('/files/upload', {
333 | data: form,
334 | headers: {
335 | 'Content-Type': 'multipart/form-data',
336 | },
337 | })
338 | }
339 | }
340 |
--------------------------------------------------------------------------------
/src/resources/Files/types.ts:
--------------------------------------------------------------------------------
1 | export type FileType =
2 | | 'FOLDER'
3 | | 'FILE'
4 | | 'AUDIO'
5 | | 'VIDEO'
6 | | 'IMAGE'
7 | | 'ARCHIVE'
8 | | 'PDF'
9 | | 'TEXT'
10 | | 'SWF'
11 |
12 | export const FileSortOptions = {
13 | NAME_ASC: 'NAME_ASC',
14 | NAME_DESC: 'NAME_DESC',
15 | SIZE_ASC: 'SIZE_ASC',
16 | SIZE_DESC: 'SIZE_DESC',
17 | DATE_ASC: 'DATE_ASC',
18 | DATE_DESC: 'DATE_DESC',
19 | MODIFIED_ASC: 'MODIFIED_ASC',
20 | MODIFIED_DESC: 'MODIFIED_DESC',
21 | } as const
22 |
23 | export type FileSortOption = typeof FileSortOptions[keyof typeof FileSortOptions]
24 |
25 | export interface IFile extends Record {
26 | id: number
27 | parent_id: number
28 | name: string
29 | size: number
30 | file_type: FileType
31 | content_type: string
32 | extension: string
33 | crc32: string
34 | created_at: string
35 | }
36 |
37 | export interface ISearchResponse {
38 | files: IFile[]
39 | total: number
40 | cursor: string
41 | }
42 |
43 | export interface IFileDeleteResponse {
44 | skipped: number
45 | cursor: string
46 | }
47 |
48 | export type FileConversionStatus = { id: IFile['id'] } & (
49 | | { status: 'NOT_AVAILABLE' }
50 | | { status: 'IN_QUEUE' }
51 | | { status: 'CONVERTING'; percent_done: number }
52 | | { status: 'COMPLETED'; size: IFile['size'] }
53 | | { status: 'ERROR' }
54 | )
55 |
--------------------------------------------------------------------------------
/src/resources/FriendInvites/FriendInvites.ts:
--------------------------------------------------------------------------------
1 | import { PutioAPIClient } from '../../client'
2 | import { IFriendInvitesCreateResponse, IFriendInvitesResponse } from './types'
3 |
4 | export default class FriendInvites {
5 | private client: PutioAPIClient
6 |
7 | constructor(client: PutioAPIClient) {
8 | this.client = client
9 | }
10 |
11 | public GetAll() {
12 | return this.client.get('/account/friend_invites')
13 | }
14 |
15 | public Create() {
16 | return this.client.post(
17 | '/account/create_friend_invitation',
18 | )
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/resources/FriendInvites/types.ts:
--------------------------------------------------------------------------------
1 | export type FriendInviteJoinedUserStatus =
2 | | 'CONVERTED'
3 | | 'IN_TRIAL'
4 | | 'TRIAL_ENDED'
5 | | 'TRIAL_NOT_STARTED'
6 |
7 | export interface IFriendInviteJoinedUser {
8 | name: string
9 | avatar_url: string
10 | created_at: string
11 | earned_amount: number
12 | status: FriendInviteJoinedUserStatus
13 | }
14 |
15 | export interface IFriendInvite {
16 | code: string
17 | created_at: string
18 | user?: IFriendInviteJoinedUser
19 | }
20 |
21 | export interface IFriendInvitesResponse {
22 | invites: IFriendInvite[]
23 | remaining_limit: number
24 | }
25 |
26 | export interface IFriendInvitesCreateResponse {
27 | code: string
28 | }
29 |
--------------------------------------------------------------------------------
/src/resources/Friends/Friends.ts:
--------------------------------------------------------------------------------
1 | import { PutioAPIClient } from '../../client'
2 | import { IFriendListResponse, IUserSearchResponse } from './types'
3 |
4 | export default class Friends {
5 | private client: PutioAPIClient
6 |
7 | constructor(client: PutioAPIClient) {
8 | this.client = client
9 | }
10 |
11 | public Query() {
12 | return this.client.get('/friends/list')
13 | }
14 |
15 | public Search(username: string) {
16 | return this.client.get(
17 | `/friends/user-search/${username}`,
18 | )
19 | }
20 |
21 | public WaitingRequests() {
22 | return this.client.get('/friends/waiting-requests')
23 | }
24 |
25 | public WaitingRequestsCount() {
26 | return this.client.get('/friends/waiting-requests-count')
27 | }
28 |
29 | public SendFrienshipRequest(username: string) {
30 | return this.client.post(`/friends/${username}/request`)
31 | }
32 |
33 | public Remove(username: string) {
34 | return this.client.post(`/friends/${username}/unfriend`)
35 | }
36 |
37 | public Approve(username: string) {
38 | return this.client.post(`/friends/${username}/approve`)
39 | }
40 |
41 | public Deny(username: string) {
42 | return this.client.post(`/friends/${username}/deny`)
43 | }
44 |
45 | public SharedFolder(username: string) {
46 | return this.client.get(`/friends/${username}/files`)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/resources/Friends/types.ts:
--------------------------------------------------------------------------------
1 | export interface IFriend {
2 | id: number
3 | name: string
4 | avatar_url: string
5 | has_received_files: boolean
6 | has_shared_files: boolean
7 | }
8 |
9 | export interface IFriendListResponse {
10 | friends: IFriend[]
11 | total: number
12 | }
13 |
14 | export interface IUserSearchResult
15 | extends Pick {
16 | invited: boolean
17 | }
18 |
19 | export interface IUserSearchResponse {
20 | users: IUserSearchResult[]
21 | }
22 |
--------------------------------------------------------------------------------
/src/resources/IFTTT.ts:
--------------------------------------------------------------------------------
1 | import { PutioAPIClient } from '../client'
2 |
3 | export default class IFTTT {
4 | private client: PutioAPIClient
5 |
6 | constructor(client: PutioAPIClient) {
7 | this.client = client
8 | }
9 |
10 | public SendEvent({
11 | clientName,
12 | eventType,
13 | ingredients = {},
14 | }: {
15 | clientName?: string
16 | eventType: string
17 | ingredients: object
18 | }) {
19 | return this.client.post('/ifttt-client/event', {
20 | data: {
21 | client_name: clientName,
22 | event_type: eventType,
23 | ingredients,
24 | },
25 | })
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/resources/Payment/Payment.ts:
--------------------------------------------------------------------------------
1 | import { PutioAPIClient } from '../../client'
2 | import {
3 | IUserPaymentInfoResponse,
4 | IVoucherInfoResponse,
5 | INanoPaymentRequestResponse,
6 | IOpenNodeChargeResponse,
7 | IPaymentOptionsResponse,
8 | IChangePlanGetResponse,
9 | IChangePlanPostResponse,
10 | IChangePlanRequestParams,
11 | } from './types'
12 |
13 | export default class Payment {
14 | private client: PutioAPIClient
15 |
16 | constructor(client: PutioAPIClient) {
17 | this.client = client
18 | }
19 |
20 | public Info() {
21 | return this.client.get('/payment/info')
22 | }
23 |
24 | public Plans() {
25 | return this.client.get('/payment/plans')
26 | }
27 |
28 | public Options() {
29 | return this.client.get('/payment/options')
30 | }
31 |
32 | public History({ unReportedOnly = false } = {}) {
33 | return this.client.get('/payment/history', {
34 | params: {
35 | unreported_only: unReportedOnly,
36 | },
37 | })
38 | }
39 |
40 | public Invites() {
41 | return this.client.get('/payment/invites')
42 | }
43 |
44 | public ChangePlan = {
45 | GET: (params: IChangePlanRequestParams) => {
46 | return this.client.get(
47 | `/payment/change_plan/${params.plan_path}`,
48 | {
49 | params: {
50 | payment_type: params.payment_type,
51 | coupon_code: params.coupon_code,
52 | },
53 | },
54 | )
55 | },
56 |
57 | POST: (params: IChangePlanRequestParams) => {
58 | return this.client.post(
59 | `/payment/change_plan/${params.plan_path}`,
60 | {
61 | data: {
62 | payment_type: params.payment_type,
63 | confirmation_code: params.confirmation_code,
64 | },
65 | params: {
66 | coupon_code: params.coupon_code,
67 | },
68 | },
69 | )
70 | },
71 | }
72 |
73 | public CreateNanoPaymentRequest({ planCode }: { planCode: string }) {
74 | return this.client.post(
75 | '/payment/methods/nano/request',
76 | {
77 | data: {
78 | plan_code: planCode,
79 | },
80 | },
81 | )
82 | }
83 |
84 | public CreateOpenNodeCharge({ planPath }: { planPath: string }) {
85 | return this.client.post(
86 | '/payment/methods/opennode/charge',
87 | {
88 | data: {
89 | plan_fs_path: planPath,
90 | },
91 | },
92 | )
93 | }
94 |
95 | public CancelSubscription() {
96 | return this.client.post('/payment/stop_subscription')
97 | }
98 |
99 | public GetVoucherInfo(code: string) {
100 | return this.client.get(
101 | `/payment/redeem_voucher/${code}`,
102 | )
103 | }
104 |
105 | public RedeemVoucher(code: string) {
106 | return this.client.post(`/payment/redeem_voucher/${code}`)
107 | }
108 |
109 | public Report(paymentIds = []) {
110 | return this.client.post('/payment/report', {
111 | data: {
112 | payment_ids: paymentIds.join(','),
113 | },
114 | })
115 | }
116 |
117 | public AddWaitingPayment(data: any) {
118 | return this.client.post('/payment/paddle_waiting_payment', { data })
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/resources/Payment/types.ts:
--------------------------------------------------------------------------------
1 | type PlanCode = string
2 | type PlanGroupCode = string
3 | type PlanName = string
4 | type PlanDiskSize = number
5 | type PlanExpirationDate = string
6 |
7 | export type PlanType = 'onetime' | 'subscription'
8 |
9 | export type UserSubscriptionStatus =
10 | | 'CANCELED'
11 | | 'PAST_DUE'
12 | | 'ACTIVE'
13 | | 'TRIALING'
14 |
15 | export interface IUserSubscription {
16 | id: number
17 | status: UserSubscriptionStatus
18 | next_billing_date?: string
19 | next_retry_date?: string
20 | update_url?: string
21 | need_payment_information_update?: boolean
22 | }
23 |
24 | export interface IUserPlan {
25 | type: PlanType
26 | code: PlanCode
27 | group_code: PlanGroupCode
28 | storage_space: number
29 | period_days?: number
30 | subscription?: IUserSubscription
31 | }
32 |
33 | export interface IUserPayment {
34 | method: string
35 | provider: string
36 | }
37 |
38 | export interface IUserPaymentInfoResponse {
39 | expiration_date: string
40 | extend_30: string
41 | extend_365: string
42 | has_pending_payment: boolean
43 | last_payment?: IUserPayment
44 | plan?: IUserPlan
45 | pending_bitpay: string | null
46 | }
47 |
48 | export interface IVoucherInfoResponse {
49 | current_plan: {
50 | type: PlanType | null
51 | expiration_date: PlanExpirationDate
52 | }
53 | target_plan: {
54 | type: PlanType
55 | name: PlanName
56 | code: PlanCode
57 | group_code: PlanGroupCode
58 | hd_avail: PlanDiskSize
59 | simulated_expiration?: PlanExpirationDate
60 | }
61 | new_remaining_days: number
62 | }
63 |
64 | export interface INanoPaymentRequestResponse {
65 | nano: {
66 | token: string
67 | }
68 | }
69 |
70 | export interface IOpenNodeChargeResponse {
71 | opennode: {
72 | checkout_url: string
73 | }
74 | }
75 |
76 | type PaymentProviderPaddle = {
77 | plan_id: number
78 | provider: 'Paddle'
79 | type: 'credit-card'
80 | vendor_id: number
81 | }
82 |
83 | type PaymentProviderFastspring = {
84 | provider: 'Fastspring'
85 | type: 'credit-card'
86 | url: string
87 | }
88 |
89 | type PaymentProviderOpenNode = {
90 | provider: 'OpenNode'
91 | type: 'cryptocurrency'
92 | discount_percent: number
93 | }
94 |
95 | type PaymentProviderAcceptNano = {
96 | amount: string
97 | api_host: string
98 | currency: 'USD'
99 | provider: 'AcceptNano'
100 | state: string
101 | type: 'nano'
102 | discount_percent: number
103 | }
104 |
105 | export type PaymentProvider =
106 | | PaymentProviderAcceptNano
107 | | PaymentProviderFastspring
108 | | PaymentProviderOpenNode
109 | | PaymentProviderPaddle
110 |
111 | export type PaymentProviderName = PaymentProvider['provider']
112 |
113 | export type PaymentType = PaymentProvider['type']
114 |
115 | export type PaymentOption = {
116 | name: PaymentType
117 | suitable_plan_types: (PlanType | 'trial')[]
118 | default?: boolean
119 | discount_percent: number
120 | }
121 |
122 | export interface IPaymentOptionsResponse {
123 | options: PaymentOption[]
124 | }
125 |
126 | export interface IChangePlanRequestParams {
127 | plan_path: string
128 | payment_type: PaymentType
129 | coupon_code?: string
130 | confirmation_code?: string
131 | }
132 |
133 | export interface IChangePlanGetResponse {
134 | amount: string
135 | current_plan: {
136 | plan_type?: PlanType
137 | subscription_payment_provider?: PaymentProviderName
138 | }
139 | target_plan: {
140 | price: string
141 | plan_type: PlanType
142 | period_days: number
143 | plan_code: PlanCode
144 | plan_name: PlanName
145 | hd_avail: PlanDiskSize
146 | simulated_expiration?: PlanExpirationDate
147 | new_code: PlanCode
148 | is_trial_subscription: boolean
149 | subscription_trial_period?: number
150 | }
151 | Paddle: {
152 | charge_amount: string
153 | currency: string
154 | next_billing_date: string
155 | }
156 | Fastspring: {
157 | prorated_amount: string
158 | refund_amount: string
159 | charge_amount: string
160 | currency: string
161 | }
162 | new_remaining_days: number
163 | prorated: string
164 | credit: string
165 | charge_amount: boolean
166 | currency: string
167 | is_product_change: boolean
168 | discount?: {
169 | discount: number
170 | type: 'percentage' | 'amount'
171 | }
172 | }
173 |
174 | type ChangePlanURLsResponse = {
175 | urls: PaymentProvider[]
176 | }
177 |
178 | type ChangePlanConfirmationResponse = {
179 | confirmation: true
180 | }
181 |
182 | type ChangePlanSubscriptionUpgradeDowngradeResponse = {
183 | charged_amount: string
184 | charged_currency: string
185 | next_payment: {
186 | amount: string
187 | billing_date: string
188 | currency: string
189 | }
190 | }
191 |
192 | type ChangePlanEmptyResponse = {}
193 |
194 | export type IChangePlanPostResponse =
195 | | ChangePlanURLsResponse
196 | | ChangePlanConfirmationResponse
197 | | ChangePlanSubscriptionUpgradeDowngradeResponse
198 | | ChangePlanEmptyResponse
199 |
--------------------------------------------------------------------------------
/src/resources/RSS/RSS.ts:
--------------------------------------------------------------------------------
1 | import { PutioAPIClient } from '../../client'
2 | import { RSSFeed, RSSFeedItem, RSSFeedParams } from './types'
3 |
4 | export default class RSS {
5 | private client: PutioAPIClient
6 |
7 | constructor(client: PutioAPIClient) {
8 | this.client = client
9 | }
10 |
11 | public Query() {
12 | return this.client.get<{ feeds: RSSFeed[] }>('/rss/list')
13 | }
14 |
15 | public Get(id: RSSFeed['id']) {
16 | return this.client.get<{ feed: RSSFeed }>(`/rss/${id}`)
17 | }
18 |
19 | public Create(rss: RSSFeedParams) {
20 | return this.client.post<{ feed: RSSFeed }>('/rss/create', {
21 | data: rss,
22 | })
23 | }
24 |
25 | public Update(id: RSSFeed['id'], rss: RSSFeedParams) {
26 | return this.client.post<{}>(`/rss/${id}`, {
27 | data: rss,
28 | })
29 | }
30 |
31 | public Pause(id: RSSFeed['id']) {
32 | return this.client.post<{}>(`/rss/${id}/pause`)
33 | }
34 |
35 | public Resume(id: RSSFeed['id']) {
36 | return this.client.post<{}>(`/rss/${id}/resume`)
37 | }
38 |
39 | public Delete(id: RSSFeed['id']) {
40 | return this.client.post<{}>(`/rss/${id}/delete`)
41 | }
42 |
43 | public Logs(id: RSSFeed['id']) {
44 | return this.client.get<{
45 | feed: RSSFeed
46 | items: RSSFeedItem[]
47 | }>(`/rss/${id}/items`)
48 | }
49 |
50 | public ClearLogs(id: RSSFeed['id']) {
51 | return this.client.post<{}>(`/rss/${id}/clear-log`)
52 | }
53 |
54 | public RetryItem(id: RSSFeed['id'], itemId: RSSFeedItem['id']) {
55 | return this.client.post<{}>(`/rss/${id}/items/${itemId}/retry`)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/resources/RSS/types.ts:
--------------------------------------------------------------------------------
1 | import { IFile } from '../Files/types'
2 |
3 | export type RSSFeed = {
4 | created_at: string
5 | delete_old_files: boolean
6 | extract: boolean
7 | id: number
8 | keyword: string | null
9 | last_error: string | null
10 | last_fetch: string | null
11 | parent_dir_id: IFile['id']
12 | parentdirid: IFile['id']
13 | paused: boolean
14 | paused_at: string | null
15 | rss_source_url: string
16 | start_at: string | null
17 | title: string
18 | unwanted_keywords: string
19 | updated_at: string
20 | }
21 |
22 | export type RSSFeedParams = Pick<
23 | RSSFeed,
24 | | 'title'
25 | | 'rss_source_url'
26 | | 'parent_dir_id'
27 | | 'delete_old_files'
28 | | 'keyword'
29 | | 'unwanted_keywords'
30 | > & {
31 | dont_process_whole_feed: boolean
32 | }
33 |
34 | type RSSFeedItemCommon = {
35 | detected_date: string
36 | id: number
37 | publish_date: string
38 | title: string
39 | processed_at: string
40 | }
41 |
42 | export type RSSFeedItemSucceeded = RSSFeedItemCommon & {
43 | is_failed: false
44 | user_file_id: null | number
45 | }
46 |
47 | export type RSSFeedItemFailed = RSSFeedItemCommon & {
48 | is_failed: true
49 | failure_reason: string
50 | }
51 |
52 | export type RSSFeedItem = RSSFeedItemSucceeded | RSSFeedItemFailed
53 |
--------------------------------------------------------------------------------
/src/resources/Sharing/Sharing.ts:
--------------------------------------------------------------------------------
1 | import { PutioAPIClient } from '../../client'
2 | import { ISharingCloneResponse, ISharingCloneInfoResponse } from './types'
3 |
4 | export default class Sharing {
5 | private client: PutioAPIClient
6 |
7 | constructor(client: PutioAPIClient) {
8 | this.client = client
9 | }
10 |
11 | public Clone({
12 | ids = [],
13 | cursor,
14 | excludeIds = [],
15 | parentId = 0,
16 | }: {
17 | ids?: number[]
18 | cursor?: string
19 | excludeIds?: number[]
20 | parentId?: number
21 | }) {
22 | return this.client.post('/sharing/clone', {
23 | data: {
24 | file_ids: ids.join(','),
25 | exclude_ids: excludeIds.join(','),
26 | parent_id: parentId,
27 | cursor,
28 | },
29 | })
30 | }
31 |
32 | public CloneInfo(cloneInfoId: number) {
33 | return this.client.get(
34 | `/sharing/clone/${cloneInfoId}`,
35 | )
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/resources/Sharing/types.ts:
--------------------------------------------------------------------------------
1 | export interface ISharingCloneResponse {
2 | id: number
3 | }
4 |
5 | export interface ISharingCloneInfoResponse {
6 | shared_file_clone_status: SharingCloneInfoStatuses
7 | }
8 |
9 | export type SharingCloneInfoStatuses = 'NEW' | 'PROCESSING' | 'DONE' | 'ERROR'
10 |
--------------------------------------------------------------------------------
/src/resources/Transfers/Transfers.ts:
--------------------------------------------------------------------------------
1 | import { PutioAPIClient } from '../../client'
2 | import {
3 | ITransfersResponse,
4 | Transfer,
5 | NewTransferParams,
6 | NewTransferError,
7 | } from './types'
8 |
9 | export default class Tranfers {
10 | private client: PutioAPIClient
11 |
12 | constructor(client: PutioAPIClient) {
13 | this.client = client
14 | }
15 |
16 | public Add(params: NewTransferParams) {
17 | return this.client.post<{ transfer: Transfer }>('/transfers/add', {
18 | data: params,
19 | })
20 | }
21 |
22 | public AddMulti(params: NewTransferParams[]) {
23 | return this.client.post<{
24 | transfers: Transfer[]
25 | errors: NewTransferError[]
26 | }>('/transfers/add-multi', {
27 | data: {
28 | urls: JSON.stringify(params),
29 | },
30 | })
31 | }
32 |
33 | public Get(id: number) {
34 | return this.client.get<{ transfer: Transfer }>(`/transfers/${id}`)
35 | }
36 |
37 | public Query({ perPage, total }: { perPage?: number; total?: boolean } = {}) {
38 | return this.client.get('/transfers/list', {
39 | params: {
40 | per_page: perPage,
41 | total,
42 | },
43 | })
44 | }
45 |
46 | public Continue(cursor: string, { perPage }: { perPage?: number } = {}) {
47 | return this.client.post('/transfers/list/continue', {
48 | data: {
49 | cursor,
50 | per_page: perPage,
51 | },
52 | })
53 | }
54 |
55 | public ClearAll() {
56 | return this.client.post<{}>('/transfers/clean')
57 | }
58 |
59 | public Cancel(ids: number[] = []) {
60 | return this.client.post<{}>('/transfers/cancel', {
61 | data: {
62 | transfer_ids: ids.join(','),
63 | },
64 | })
65 | }
66 |
67 | public Retry(id: number) {
68 | return this.client.post('/transfers/retry', {
69 | data: { id },
70 | })
71 | }
72 |
73 | public Reannounce(id: number) {
74 | return this.client.post('/transfers/reannounce', {
75 | data: { id },
76 | })
77 | }
78 |
79 | public Count() {
80 | return this.client.get<{ count: number }>('/transfers/count')
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/resources/Transfers/types.ts:
--------------------------------------------------------------------------------
1 | import { ISODateString } from '../../utils/types'
2 | import { IFile } from '../../resources/Files/types'
3 |
4 | export type NewTransferParams = {
5 | url: string
6 | save_parent_id?: IFile['id']
7 | callback_url?: string
8 | }
9 |
10 | export type NewTransferError = {
11 | url: NewTransferParams['url']
12 | status_code: number
13 | error_type: string
14 | }
15 |
16 | type TransferType = 'URL' | 'TORRENT' | 'PLAYLIST' | 'LIVE_STREAM' | 'N/A'
17 |
18 | type TransferStatus =
19 | | 'WAITING'
20 | | 'PREPARING_DOWNLOAD'
21 | | 'IN_QUEUE'
22 | | 'DOWNLOADING'
23 | | 'WAITING_FOR_COMPLETE_QUEUE'
24 | | 'WAITING_FOR_DOWNLOADER'
25 | | 'COMPLETING'
26 | | 'STOPPING'
27 | | 'SEEDING'
28 | | 'COMPLETED'
29 | | 'ERROR'
30 |
31 | type TransferLink = { url: string; label: string }
32 |
33 | type BaseTransfer = {
34 | availability: null | number
35 | created_at: ISODateString
36 | current_ratio: null | number
37 | down_speed: number
38 | downloaded: number
39 | error_message: null | string
40 | estimated_time: null | number
41 | file_id: null | number
42 | finished_at: null | ISODateString
43 | id: number
44 | is_private: boolean
45 | name: string
46 | save_parent_id: number
47 | seconds_seeding: null | number
48 | size: number
49 | source: string
50 | started_at: null | ISODateString
51 | status: TransferStatus
52 | type: TransferType
53 | subscription_id: null | number // ID of the RSS Feed
54 | uploaded: number
55 | up_speed: number
56 |
57 | // not documented on api.put.io
58 | callback_url: null | string // not used in clients
59 | client_ip: null | string
60 |
61 | completion_percent: number // ⬇️ are they different?
62 | percent_done: number // ⬆️
63 |
64 | created_torrent: boolean // not used in clients
65 | download_id: number // not used in clients
66 | hash: null | string // not used in clients
67 | links: TransferLink[] // used in admin
68 | peers_connected: number
69 | peers_getting_from_us: number // not used in clients
70 | peers_sending_to_us: number // not used in clients
71 | simulated: boolean // not used in clients
72 | torrent_link: null | string
73 | tracker: null | string // not used in clients
74 | tracker_message: null | string
75 |
76 | updated_at?: ISODateString // not used in clients
77 |
78 | userfile_exists?: boolean
79 | }
80 |
81 | type FinishedTransfer = BaseTransfer & {
82 | finished_at: ISODateString
83 | file_id: number
84 | }
85 |
86 | /* by status */
87 | type CompletedTransfer = FinishedTransfer & {
88 | status: 'COMPLETED'
89 | }
90 |
91 | type FailedTransfer = BaseTransfer & {
92 | status: 'ERROR'
93 | error_message: string
94 | }
95 |
96 | /* by status, but scoped to type */
97 | type PreparingSeedingTorrentTransfer = FinishedTransfer & {
98 | type: 'TORRENT'
99 | status: 'PREPARING_SEED'
100 | }
101 |
102 | type SeedingTorrentTransfer = FinishedTransfer & {
103 | type: 'TORRENT'
104 | status: 'SEEDING'
105 | seconds_seeding: number
106 | current_ratio: number
107 | }
108 |
109 | type CompletedTorrentTransfer = SeedingTorrentTransfer & {
110 | status: 'COMPLETED'
111 | }
112 |
113 | /* by type */
114 | export type TorrentTransfer = (
115 | | BaseTransfer
116 | | FailedTransfer
117 | | SeedingTorrentTransfer
118 | | PreparingSeedingTorrentTransfer
119 | | CompletedTorrentTransfer
120 | ) & {
121 | type: 'TORRENT'
122 | torrent_link: string
123 | }
124 |
125 | export type URLTransfer = (
126 | | BaseTransfer
127 | | FailedTransfer
128 | | CompletedTransfer
129 | ) & {
130 | type: 'URL'
131 | }
132 |
133 | export type PlaylistTransfer = (
134 | | BaseTransfer
135 | | FailedTransfer
136 | | CompletedTransfer
137 | ) & {
138 | type: 'PLAYLIST'
139 |
140 | // not documented in swagger
141 | // not present in transfers/list response, but comes within socket updates.
142 | downloaded_items: number
143 | total_items: number
144 |
145 | // why present in this one? it's not live recording.
146 | recorded_seconds: number
147 | }
148 |
149 | export type LiveStreamTransfer = (
150 | | BaseTransfer
151 | | FailedTransfer
152 | | CompletedTransfer
153 | ) & {
154 | type: 'LIVE_STREAM'
155 |
156 | // not documented in swagger
157 | recorded_seconds: number
158 | }
159 |
160 | export type Transfer =
161 | | TorrentTransfer
162 | | URLTransfer
163 | | PlaylistTransfer
164 | | LiveStreamTransfer
165 |
166 | export interface ITransfersResponse {
167 | transfers: Transfer[]
168 | total: number
169 | cursor: string
170 | }
171 |
--------------------------------------------------------------------------------
/src/resources/Trash.ts:
--------------------------------------------------------------------------------
1 | import { PutioAPIClient } from '../client'
2 |
3 | export default class Trash {
4 | private client: PutioAPIClient
5 |
6 | constructor(client: PutioAPIClient) {
7 | this.client = client
8 | }
9 |
10 | public List({ limit = 50 } = {}) {
11 | return this.client.get('/trash/list', {
12 | params: {
13 | per_page: limit,
14 | },
15 | })
16 | }
17 |
18 | public Continue(cursor: string, { limit = 50 } = {}) {
19 | return this.client.post('/trash/list/continue', {
20 | data: {
21 | cursor,
22 | per_page: limit,
23 | },
24 | })
25 | }
26 |
27 | public Restore({
28 | useCursor = false,
29 | ids = [],
30 | cursor,
31 | }: {
32 | useCursor?: boolean
33 | ids?: number[]
34 | cursor?: string
35 | }) {
36 | return this.client.post('/trash/restore', {
37 | data: {
38 | cursor: useCursor ? cursor : undefined,
39 | file_ids: !useCursor ? ids.join(',') : undefined,
40 | },
41 | })
42 | }
43 |
44 | public Delete({
45 | useCursor = false,
46 | ids = [],
47 | cursor,
48 | }: {
49 | useCursor?: boolean
50 | ids?: number[]
51 | cursor?: string
52 | }) {
53 | return this.client.post('/trash/delete', {
54 | data: {
55 | cursor: useCursor ? cursor : undefined,
56 | file_ids: !useCursor ? ids.join(',') : undefined,
57 | },
58 | })
59 | }
60 |
61 | public Empty() {
62 | return this.client.post('/trash/empty')
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/resources/Tunnel.ts:
--------------------------------------------------------------------------------
1 | import { PutioAPIClient } from '../client'
2 |
3 | export default class Tunnel {
4 | private client: PutioAPIClient
5 |
6 | constructor(client: PutioAPIClient) {
7 | this.client = client
8 | }
9 |
10 | public Routes() {
11 | return this.client.get('/tunnel/routes')
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/resources/User/User.ts:
--------------------------------------------------------------------------------
1 | import { PutioAPIClient } from '../../client'
2 | import {
3 | IUserInfoResponse,
4 | IUserSettingsResponse,
5 | IUserConfirmationsResponse,
6 | ConfirmationSubject,
7 | ClearDataOptions,
8 | } from './types'
9 |
10 | export default class User {
11 | private client: PutioAPIClient
12 |
13 | constructor(client: PutioAPIClient) {
14 | this.client = client
15 | }
16 |
17 | /**
18 | * @deprecated Use `Account.Info` method instead.
19 | */
20 | public Info(params: Record) {
21 | return this.client.get('/account/info', {
22 | params,
23 | })
24 | }
25 |
26 | /**
27 | * @deprecated Use `Account.Settings` method instead.
28 | */
29 | public Settings() {
30 | return this.client.get('/account/settings')
31 | }
32 |
33 | /**
34 | * @deprecated Use `Account.SaveSettings` method instead.
35 | */
36 | public SaveSettings(settings: Record) {
37 | return this.client.post('/account/settings', {
38 | data: settings,
39 | })
40 | }
41 |
42 | /**
43 | * @deprecated Use `Config.Read` method instead.
44 | */
45 | public Config() {
46 | return this.client.get>('/config')
47 | }
48 |
49 | /**
50 | * @deprecated Use `Config.Write` method instead.
51 | */
52 | public SaveConfig(config: Record) {
53 | return this.client.put('/config', {
54 | data: { config },
55 | })
56 | }
57 |
58 | /**
59 | * @deprecated Use `Account.Clear` method instead.
60 | */
61 | public ClearData(options: ClearDataOptions) {
62 | return this.client.post<{}>('/account/clear', {
63 | data: options,
64 | })
65 | }
66 |
67 | /**
68 | * @deprecated Use `Account.Destroy` method instead.
69 | */
70 | public Destroy(currentPassword: string) {
71 | return this.client.post<{}>('/account/destroy', {
72 | data: {
73 | current_password: currentPassword,
74 | },
75 | })
76 | }
77 |
78 | /**
79 | * @deprecated Use `Account.Confirmations` method instead.
80 | */
81 | public Confirmations(type?: ConfirmationSubject) {
82 | return this.client.get(
83 | '/account/confirmation/list',
84 | {
85 | data: {
86 | type,
87 | },
88 | },
89 | )
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/resources/User/types.ts:
--------------------------------------------------------------------------------
1 | export interface IUserDisk {
2 | avail: number
3 | used: number
4 | size: number
5 | }
6 |
7 | export interface IUserProperties extends Record {
8 | user_id: number
9 | username: string
10 | mail: string
11 | avatar_url: string
12 | account_active: boolean
13 | disk: IUserDisk
14 | is_sub_account: boolean
15 | is_eligible_for_friend_invitation: boolean
16 | is_admin: boolean
17 | can_create_sub_account: boolean
18 | files_will_be_deleted_at: string | null
19 | password_last_changed_at: string | null
20 | }
21 |
22 | export interface IUserSettings extends Record {
23 | default_download_folder: number
24 | dark_theme: boolean
25 | fluid_layout: boolean
26 | history_enabled: boolean
27 | show_optimistic_usage: boolean
28 | disable_subtitles: boolean
29 | is_invisible: boolean
30 | start_from: boolean
31 | trash_enabled: boolean
32 | two_factor_enabled: boolean
33 | }
34 |
35 | export interface IUser extends IUserProperties {
36 | settings: IUserSettings
37 | }
38 |
39 | export interface IUserInfoResponse {
40 | info: IUser
41 | }
42 |
43 | export interface IUserSettingsResponse {
44 | settings: IUserSettings
45 | }
46 |
47 | export type ConfirmationSubject =
48 | | 'mail_change'
49 | | 'password_change'
50 | | 'subscription_upgrade'
51 |
52 | export interface IUserConfirmation {
53 | subject: ConfirmationSubject
54 | created_at: string
55 | }
56 |
57 | export interface IUserConfirmationsResponse {
58 | confirmations: IUserConfirmation[]
59 | }
60 |
61 | export type ClearDataOptions = {
62 | files: boolean
63 | finished_transfers: boolean
64 | active_transfers: boolean
65 | rss_feeds: boolean
66 | rss_logs: boolean
67 | history: boolean
68 | trash: boolean
69 | friends: boolean
70 | }
71 |
--------------------------------------------------------------------------------
/src/resources/Zips.ts:
--------------------------------------------------------------------------------
1 | import { PutioAPIClient } from '../client'
2 |
3 | export default class Zips {
4 | private client: PutioAPIClient
5 |
6 | constructor(client: PutioAPIClient) {
7 | this.client = client
8 | }
9 |
10 | public Query() {
11 | return this.client.get('/zips/list')
12 | }
13 |
14 | public Create({
15 | cursor,
16 | excludeIds = [],
17 | ids = [],
18 | }: {
19 | cursor?: string
20 | excludeIds?: number[]
21 | ids: number[]
22 | }) {
23 | return this.client.post('/zips/create', {
24 | data: {
25 | cursor,
26 | exclude_ids: excludeIds.join(','),
27 | file_ids: ids.join(','),
28 | },
29 | })
30 | }
31 |
32 | public Get(id: number) {
33 | return this.client.get(`/zips/${id}`)
34 | }
35 |
36 | public Retry(id: number) {
37 | return this.client.get(`/zips/${id}/retry`)
38 | }
39 |
40 | public Cancel(id: number) {
41 | return this.client.get(`/zips/${id}/cancel`)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/test-utils/mocks.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError, AxiosRequestConfig } from 'axios'
2 | import {
3 | IPutioAPIClientError,
4 | IPutioAPIClientErrorData,
5 | IPutioAPIClientResponse,
6 | } from '../client/types'
7 |
8 | const mockRequestConfig: AxiosRequestConfig = {}
9 |
10 | export const mockPutioAPIClientResponse: IPutioAPIClientResponse<{
11 | foo: string
12 | }> = {
13 | config: mockRequestConfig,
14 | data: { foo: 'bar', status: 'OK' },
15 | headers: {
16 | 'x-trace-id': 'MOCK_TRACE_ID',
17 | },
18 | status: 200,
19 | statusText: 'ok',
20 | }
21 |
22 | export const mockAxiosError: AxiosError = {
23 | config: mockRequestConfig,
24 | isAxiosError: true,
25 | name: 'AXIOS_ERROR',
26 | message: 'AXIOS_ERROR_MESSAGE',
27 | toJSON() {
28 | return {
29 | name: this.name,
30 | message: this.message,
31 | }
32 | },
33 | }
34 |
35 | export const mockPutioAPIClientError: IPutioAPIClientError = {
36 | ...mockAxiosError,
37 | data: {
38 | error_type: 'MOCK_ERROR',
39 | error_message: 'MOCK_MESSAGE',
40 | status_code: 0,
41 | extra: { foo: 'bar' },
42 | },
43 | toJSON() {
44 | return this.data
45 | },
46 | }
47 |
48 | export const createMockResponse = (
49 | data: T,
50 | status: number = 200,
51 | ): IPutioAPIClientResponse => ({
52 | config: mockRequestConfig,
53 | data: { ...data, status: 'OK' },
54 | status,
55 | headers: {},
56 | statusText: 'ok',
57 | })
58 |
59 | export const createMockErrorResponse = (
60 | data: IPutioAPIClientErrorData,
61 | ): IPutioAPIClientError => ({
62 | ...mockAxiosError,
63 | data,
64 | toJSON() {
65 | return this.data
66 | },
67 | })
68 |
69 | export function createMockXMLHttpRequest(
70 | readyState: number,
71 | status: number,
72 | responseText: string,
73 | ) {
74 | const xhr = new XMLHttpRequest()
75 | return new Proxy(xhr, {
76 | get(target, prop) {
77 | if (prop === 'readyState') return readyState
78 | if (prop === 'status') return status
79 | if (prop === 'responseText') return responseText
80 | // @ts-ignore
81 | return target[prop]
82 | },
83 | })
84 | }
85 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | import { IPutioAPIClientError, IPutioAPIClientErrorData } from '../client/types'
2 |
3 | export const identity = (arg: T) => arg
4 |
5 | export const isPutioAPIErrorResponse = (
6 | input: unknown,
7 | ): input is IPutioAPIClientErrorData =>
8 | typeof input === 'object' &&
9 | !!input &&
10 | !!(input as Record).error_type
11 |
12 | export const isPutioAPIError = (
13 | input: unknown,
14 | ): input is IPutioAPIClientError =>
15 | typeof input === 'object' &&
16 | !!input &&
17 | !!(input as Record).data &&
18 | isPutioAPIErrorResponse((input as Record).data)
19 |
20 | export const createFormDataFromObject = (obj: Record) => {
21 | return Object.keys(obj).reduce((data, key) => {
22 | data.append(key, obj[key] as string | Blob)
23 | return data
24 | }, new FormData())
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/types.ts:
--------------------------------------------------------------------------------
1 | export type ISODateString = string
2 |
--------------------------------------------------------------------------------
/src/utils/utils.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createFormDataFromObject,
3 | isPutioAPIError,
4 | isPutioAPIErrorResponse,
5 | } from '.'
6 | import {
7 | mockAxiosError,
8 | mockPutioAPIClientError,
9 | mockPutioAPIClientResponse,
10 | } from '../test-utils/mocks'
11 |
12 | describe('utils', () => {
13 | describe('isPutioAPIErrorResponse helper', () => {
14 | it('validates an object matching the expected shape', () => {
15 | expect(isPutioAPIErrorResponse(mockPutioAPIClientError.data)).toBe(true)
16 | })
17 |
18 | it('validates an object not matching the expected shape', () => {
19 | expect(isPutioAPIErrorResponse(mockPutioAPIClientResponse.data)).toBe(
20 | false,
21 | )
22 | })
23 |
24 | it('validates non-object inputs', () => {
25 | expect(isPutioAPIErrorResponse('foo')).toBe(false)
26 | })
27 | })
28 |
29 | describe('isPutioAPIError helper', () => {
30 | it('validates an object matching the expected shape', () => {
31 | expect(isPutioAPIError(mockPutioAPIClientError)).toBe(true)
32 | })
33 |
34 | it('validates an object not matching the expected shape', () => {
35 | expect(isPutioAPIError(mockAxiosError)).toBe(false)
36 | })
37 |
38 | it('validates an error not matching the expected shape', () => {
39 | expect(isPutioAPIError(new Error('Error!'))).toBe(false)
40 | })
41 |
42 | it('validates non-object inputs', () => {
43 | expect(isPutioAPIError('foo')).toBe(false)
44 | })
45 | })
46 |
47 | describe('createFormDataFromObject helper', () => {
48 | it('creates form data from the given object', () => {
49 | expect(createFormDataFromObject({ foo: 'bar' }).get('foo')).toBe('bar')
50 | })
51 | })
52 | })
53 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src"],
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "declaration": true,
6 | "esModuleInterop": true,
7 | "importHelpers": true,
8 | "jsx": "react",
9 | "lib": ["dom", "esnext"],
10 | "module": "esnext",
11 | "moduleResolution": "node",
12 | "noFallthroughCasesInSwitch": true,
13 | "noImplicitReturns": true,
14 | "noUnusedLocals": true,
15 | "noUnusedParameters": true,
16 | "paths": { "*": ["src/*", "node_modules/*"] },
17 | "rootDir": "./src",
18 | "sourceMap": true,
19 | "strict": true,
20 | "strictNullChecks": true
21 | }
22 | }
23 |
--------------------------------------------------------------------------------