├── .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 | Build Status 16 | Coverage Status 18 | npm (scoped) 19 | npm bundle size (scoped) 20 | GitHub 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 | --------------------------------------------------------------------------------