From bce0115285dce6caa3f75378e3e9e7b33980e8cb Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Mon, 21 Sep 2020 17:11:16 -0700 Subject: [PATCH 001/680] Generate types from OpenAPI --- .gitignore | 3 +- Public API v1.yaml | 250 ++++++++++++++++----------------- package-lock.json | 97 +++++++++++++ package.json | 5 +- src/app/main/main.component.ts | 2 +- src/app/posts.services.ts | 53 +++---- 6 files changed, 257 insertions(+), 153 deletions(-) diff --git a/.gitignore b/.gitignore index 9d2ac87..32c9fd9 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ backend/subscriptions/playlists/* backend/subscriptions/archives/* backend/*.exe src/assets/default.json +src/api-types backend/appdata/db.json backend/appdata/archives/archive_audio.txt backend/appdata/archives/archive_video.txt @@ -65,4 +66,4 @@ backend/appdata/logs/error.log backend/appdata/users.json backend/users/* backend/appdata/cookies.txt -backend/public \ No newline at end of file +backend/public diff --git a/Public API v1.yaml b/Public API v1.yaml index e82328a..15013e0 100644 --- a/Public API v1.yaml +++ b/Public API v1.yaml @@ -21,14 +21,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body' + $ref: '#/components/schemas/Mp3DownloadRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/inline_response_200' + $ref: '#/components/schemas/Mp3DownloadResponse' + '500': + description: Server download error security: - Auth query parameter: [] /api/tomp4: @@ -46,14 +48,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body_1' + $ref: '#/components/schemas/Mp4DownloadRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_1' + $ref: '#/components/schemas/Mp4DownloadResponse' + '500': + description: Server download error security: - Auth query parameter: [] /api/getMp3s: @@ -69,8 +73,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_2' - requestBody: {} + $ref: '#/components/schemas/GetMp3sResponse' security: - Auth query parameter: [] /api/getMp4s: @@ -86,7 +89,23 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_3' + $ref: '#/components/schemas/GetMp4sResponse' + security: + - Auth query parameter: [] + /api/getAllFiles: + post: + tags: + - files + summary: Get all files + description: Gets all files and playlists stored in the db + operationId: get-getAllFiles + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GetAllFilesResponse' security: - Auth query parameter: [] /api/getFile: @@ -100,14 +119,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body_2' + $ref: '#/components/schemas/GetFileRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_4' + $ref: '#/components/schemas/GetFileResponse' security: - Auth query parameter: [] /api/enableSharing: @@ -122,7 +141,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body_3' + $ref: '#/components/schemas/SharingToggle' responses: '200': description: OK @@ -144,7 +163,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body_4' + $ref: '#/components/schemas/SharingToggle' responses: '200': description: OK @@ -718,7 +737,7 @@ paths: type: object properties: user: - $ref: '#/components/schemas/user' + $ref: '#/components/schemas/User' token: type: string permissions: @@ -761,7 +780,7 @@ paths: type: object properties: user: - $ref: '#/components/schemas/user' + $ref: '#/components/schemas/User' description: Use this endpoint to register a user. It will only work if registration is enabled. requestBody: content: @@ -958,7 +977,7 @@ paths: users: type: array items: - $ref: '#/components/schemas/user' + $ref: '#/components/schemas/User' description: 'Gets all users, returns a list of the user objects including their user permissions, videos, playlists, subscriptions, etc.' security: - Auth query parameter: [] @@ -966,7 +985,12 @@ paths: - multi-user mode components: schemas: - body: + FileType: + type: string + enum: + - audio + - video + BaseDownloadRequest: required: - url type: object @@ -975,64 +999,72 @@ components: type: string customQualityConfiguration: type: string + description: Video format code. Overrides other quality options. example: '251' - maxBitrate: - type: string - example: '160' customArgs: type: string + description: Custom command-line arguments for youtubedl. Overrides all other options, except url. customOutput: type: string + description: Custom output filename template. youtubeUsername: type: string + description: Login with this account ID youtubePassword: type: string - inline_response_200: + description: Account password + ui_uid: + type: string + nullable: true + Mp3DownloadRequest: + allOf: + - $ref: '#/components/schemas/BaseDownloadRequest' + - type: object + properties: + maxBitrate: + type: string + description: Specify ffmpeg/avconv audio quality + example: '160' + Mp4DownloadRequest: + allOf: + - $ref: '#/components/schemas/BaseDownloadRequest' + - type: object + properties: + selectedHeight: + type: string + description: Height of the video, if known + example: '1080' + BaseDownloadResponse: required: - - audiopathEncoded - uid type: object properties: uid: type: string file_names: - type: string - audiopathEncoded: - type: string - body_1: - required: - - url - type: object - properties: - url: - type: string - customQualityConfiguration: - type: string - example: 242+251 - selectedHeight: - type: string - example: '1080' - customArgs: - type: string - customOutput: - type: string - youtubeUsername: - type: string - youtubePassword: - type: string - inline_response_200_1: - required: - - uid - - videopathEncoded - type: object - properties: - uid: - type: string - file_names: - type: string - videopathEncoded: - type: string - inline_response_200_2: + nullable: true + type: array + items: + type: string + Mp3DownloadResponse: + allOf: + - $ref: '#/components/schemas/BaseDownloadResponse' + - type: object + required: + - audiopathEncoded + properties: + audiopathEncoded: + type: string + Mp4DownloadResponse: + allOf: + - $ref: '#/components/schemas/BaseDownloadResponse' + - type: object + required: + - videopathEncoded + properties: + videopathEncoded: + type: string + GetMp3sResponse: required: - mp3s - playlists @@ -1041,48 +1073,66 @@ components: mp3s: type: array items: - $ref: '#/components/schemas/inline_response_200_2_mp3s' + $ref: '#/components/schemas/DatabaseFile' playlists: type: array description: All audio playlists items: - $ref: '#/components/schemas/inline_response_200_2_playlists' - inline_response_200_3: + $ref: '#/components/schemas/Playlist' + GetMp4sResponse: required: - mp4s + - playlists type: object properties: mp4s: type: array items: - $ref: '#/components/schemas/inline_response_200_3_mp4s' + $ref: '#/components/schemas/DatabaseFile' playlists: type: array description: All video playlists items: - type: object - body_2: + $ref: '#/components/schemas/Playlist' + GetAllFilesResponse: + required: + - files + - playlists + type: object + properties: + files: + type: array + items: + $ref: '#/components/schemas/DatabaseFile' + playlists: + type: array + description: All video playlists + items: + $ref: '#/components/schemas/Playlist' + GetFileRequest: required: - uid type: object properties: uid: type: string + description: Video UID type: + $ref: '#/components/schemas/FileType' + uuid: type: string - inline_response_200_4: + description: User UID + GetFileResponse: required: - - file - success type: object properties: success: - type: string + type: boolean file: - $ref: '#/components/schemas/inline_response_200_2_mp3s' - body_3: + $ref: '#/components/schemas/DatabaseFile' + SharingToggle: required: - - is_playlist - type - uid type: object @@ -1090,7 +1140,7 @@ components: uid: type: string type: - type: string + $ref: '#/components/schemas/FileType' is_playlist: type: boolean inline_response_200_5: @@ -1100,19 +1150,6 @@ components: properties: success: type: boolean - body_4: - required: - - type - - uid - type: object - properties: - type: - type: string - uid: - type: string - description: uid is either the video uid or the playlist ID - is_playlist: - type: boolean body_5: required: - url @@ -1303,7 +1340,7 @@ components: type: object properties: playlist: - $ref: '#/components/schemas/inline_response_200_2_playlists' + $ref: '#/components/schemas/Playlist' type: type: string success: @@ -1457,7 +1494,7 @@ components: properties: new_config_file: type: object - inline_response_200_2_mp3s: + DatabaseFile: required: - duration - id @@ -1497,7 +1534,7 @@ components: type: string sharingEnabled: type: boolean - inline_response_200_2_playlists: + Playlist: required: - fileNames - id @@ -1515,45 +1552,6 @@ components: type: string thumbnailURL: type: string - inline_response_200_3_mp4s: - required: - - duration - - id - - isAudio - - path - - size - - thumbnailURL - - title - - uid - - upload_date - - uploader - - url - type: object - properties: - id: - type: string - title: - type: string - thumbnailURL: - type: string - isAudio: - type: boolean - duration: - type: number - url: - type: string - uploader: - type: string - size: - type: number - path: - type: string - upload_date: - type: string - uid: - type: string - sharingEnabled: - type: boolean inline_response_200_6_new_sub: required: - id @@ -1633,7 +1631,7 @@ components: type: string type: type: string - user: + User: title: user type: object properties: diff --git a/package-lock.json b/package-lock.json index 08cd667..2cfdacc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9618,6 +9618,76 @@ "is-wsl": "^2.1.1" } }, + "openapi-typescript-codegen": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/openapi-typescript-codegen/-/openapi-typescript-codegen-0.4.11.tgz", + "integrity": "sha512-KWhJE4xlFXDd7sNyEiJeUx7+H1hqQ+WETzwrcXCE+CNR+pmfKkOH87fjPRUPu1TY3hSgDgZ6ex7F3lWLbirvMQ==", + "dev": true, + "requires": { + "camelcase": "6.0.0", + "commander": "6.1.0", + "handlebars": "4.7.6", + "js-yaml": "3.14.0", + "mkdirp": "1.0.4", + "path": "0.12.7", + "rimraf": "3.0.2" + }, + "dependencies": { + "camelcase": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.0.0.tgz", + "integrity": "sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==", + "dev": true + }, + "commander": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.1.0.tgz", + "integrity": "sha512-wl7PNrYWd2y5mp1OK/LhTlv8Ff4kQJQRXXAvF+uU/TPNiVJUxZLRYGj/B0y/lPGAVcSbJqH2Za/cvHmrPMC8mA==", + "dev": true + }, + "handlebars": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "dev": true, + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, "opn": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", @@ -10048,6 +10118,33 @@ "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", "dev": true }, + "path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=", + "dev": true, + "requires": { + "process": "^0.11.1", + "util": "^0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + } + } + }, "path-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", diff --git a/package.json b/package.json index 14aa13e..0e1ff92 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "test": "ng test", "lint": "ng lint", "e2e": "ng e2e", - "electron": "ng build --base-href ./ && electron ." + "electron": "ng build --base-href ./ && electron .", + "generate": "openapi --input ./\"Public API v1.yaml\" --output ./src/api-types --exportCore false --exportServices false --exportModels true", + "prepare": "npm run generate" }, "engines": { "node": "12.3.1", @@ -66,6 +68,7 @@ "karma-coverage-istanbul-reporter": "^1.2.1", "karma-jasmine": "~1.1.0", "karma-jasmine-html-reporter": "^0.2.2", + "openapi-typescript-codegen": "^0.4.11", "protractor": "~5.1.2", "ts-node": "~3.0.4", "tslint": "~5.3.2" diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts index b444e11..3625449 100644 --- a/src/app/main/main.component.ts +++ b/src/app/main/main.component.ts @@ -63,7 +63,7 @@ export class MainComponent implements OnInit { youtubeUsername = null; youtubePassword = null; urlError = false; - path = ''; + path: string | string[] = ''; url = ''; exists = ''; percentDownloaded: number; diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 416fd2d..2153f73 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -8,9 +8,9 @@ import { THEMES_CONFIG } from '../themes'; import { Router, CanActivate } from '@angular/router'; import { DOCUMENT } from '@angular/common'; import { BehaviorSubject } from 'rxjs'; -import { v4 as uuid } from 'uuid'; import { MatSnackBar } from '@angular/material/snack-bar'; import * as Fingerprint2 from 'fingerprintjs2'; +import type { FileType, GetAllFilesResponse, GetFileRequest, GetFileResponse, GetMp3sResponse, GetMp4sResponse, Mp3DownloadRequest, Mp3DownloadResponse, Mp4DownloadRequest, Mp4DownloadResponse } from 'api-types'; @Injectable() export class PostsService implements CanActivate { @@ -25,7 +25,9 @@ export class PostsService implements CanActivate { // auth auth_token = '4241b401-7236-493e-92b5-b72696b9d853'; session_id = null; - httpOptions = null; + httpOptions: { + params: HttpParams + }; http_params: string = null; unauthorized = false; @@ -155,27 +157,29 @@ export class PostsService implements CanActivate { } // tslint:disable-next-line: max-line-length - makeMP3(url: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, ui_uid = null) { - return this.http.post(this.path + 'tomp3', {url: url, - maxBitrate: selectedQuality, - customQualityConfiguration: customQualityConfiguration, - customArgs: customArgs, - customOutput: customOutput, - youtubeUsername: youtubeUsername, - youtubePassword: youtubePassword, - ui_uid: ui_uid}, this.httpOptions); + makeMP3(url: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, ui_uid: string = null) { + const body: Mp3DownloadRequest = {url: url, + maxBitrate: selectedQuality, + customQualityConfiguration: customQualityConfiguration, + customArgs: customArgs, + customOutput: customOutput, + youtubeUsername: youtubeUsername, + youtubePassword: youtubePassword, + ui_uid: ui_uid} + return this.http.post(this.path + 'tomp3', body, this.httpOptions); } // tslint:disable-next-line: max-line-length makeMP4(url: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, ui_uid = null) { - return this.http.post(this.path + 'tomp4', {url: url, - selectedHeight: selectedQuality, - customQualityConfiguration: customQualityConfiguration, - customArgs: customArgs, - customOutput: customOutput, - youtubeUsername: youtubeUsername, - youtubePassword: youtubePassword, - ui_uid: ui_uid}, this.httpOptions); + const body: Mp4DownloadRequest = {url: url, + selectedHeight: selectedQuality, + customQualityConfiguration: customQualityConfiguration, + customArgs: customArgs, + customOutput: customOutput, + youtubeUsername: youtubeUsername, + youtubePassword: youtubePassword, + ui_uid: ui_uid} + return this.http.post(this.path + 'tomp4', body, this.httpOptions); } killAllDownloads() { @@ -207,19 +211,20 @@ export class PostsService implements CanActivate { } getMp3s() { - return this.http.get(this.path + 'getMp3s', this.httpOptions); + return this.http.get(this.path + 'getMp3s', this.httpOptions); } getMp4s() { - return this.http.get(this.path + 'getMp4s', this.httpOptions); + return this.http.get(this.path + 'getMp4s', this.httpOptions); } - getFile(uid, type, uuid = null) { - return this.http.post(this.path + 'getFile', {uid: uid, type: type, uuid: uuid}, this.httpOptions); + getFile(uid: string, type: FileType, uuid: string = null) { + const body: GetFileRequest = {uid: uid, type: type, uuid: uuid}; + return this.http.post(this.path + 'getFile', body, this.httpOptions); } getAllFiles() { - return this.http.post(this.path + 'getAllFiles', {}, this.httpOptions); + return this.http.post(this.path + 'getAllFiles', {}, this.httpOptions); } downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null, subscriptionName = null, subPlaylist = null, From b667e1dc7970bf57ca17095b6e3be77d0e3063ec Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Thu, 24 Sep 2020 14:16:13 -0700 Subject: [PATCH 002/680] Add additional named types --- Public API v1.yaml | 261 +++++++++++++++++++++----------------- src/app/posts.services.ts | 17 ++- 2 files changed, 156 insertions(+), 122 deletions(-) diff --git a/Public API v1.yaml b/Public API v1.yaml index 15013e0..2289c3b 100644 --- a/Public API v1.yaml +++ b/Public API v1.yaml @@ -395,7 +395,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body_14' + $ref: '#/components/schemas/DeleteMp3Mp4Request' responses: '200': description: OK @@ -436,7 +436,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body_16' + $ref: '#/components/schemas/DeleteFileRequest' responses: '200': description: OK @@ -581,7 +581,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body_21' + $ref: '#/components/schemas/DeleteMp3Mp4Request' responses: '200': description: OK @@ -637,33 +637,7 @@ paths: content: application/json: schema: - type: object - properties: - downloads: - type: object - properties: - uid: - type: string - downloading: - type: boolean - complete: - type: boolean - url: - type: string - type: - type: string - percent_complete: - type: string - is_playlist: - type: boolean - timestamp_start: - type: number - timestamp_end: - type: number - fileNames: - type: array - items: - type: string + $ref: '#/components/schemas/GetAllDownloadsResponse' operationId: get-api-downloads description: Retrieves all downloads recorded by the server and their status. security: @@ -678,46 +652,12 @@ paths: content: application/json: schema: - type: object - properties: - download: - type: object - properties: - uid: - type: string - downloading: - type: boolean - complete: - type: boolean - url: - type: string - type: - type: string - percent_complete: - type: string - is_playlist: - type: boolean - timestamp_start: - type: number - timestamp_end: - type: number - fileNames: - type: array - items: - type: string + $ref: '#/components/schemas/GetDownloadResponse' requestBody: content: application/json: schema: - type: object - properties: - session_id: - type: string - download_id: - type: string - required: - - session_id - - download_id + $ref: '#/components/schemas/GetDownloadRequest' description: '' description: "Gets a single download using its download_id and session_id. session_id is the device fingerprint. If none was provided at the time of download, then set session_id is 'undeclared'." security: @@ -809,6 +749,10 @@ paths: responses: '200': description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessObject' requestBody: content: application/json: @@ -816,8 +760,12 @@ paths: type: object properties: change_object: + required: + - uid type: object properties: + uid: + type: string name: type: string role: @@ -839,10 +787,7 @@ paths: content: application/json: schema: - type: object - properties: - success: - type: boolean + $ref: '#/components/schemas/SuccessObject' description: Deletes a user by its uid. requestBody: content: @@ -879,14 +824,14 @@ paths: permissions: type: array items: - type: string + $ref: '#/components/schemas/UserPermission' user: type: object properties: permissions: type: array items: - type: string + $ref: '#/components/schemas/UserPermission' description: Gets the available roles and their permissions security: - Auth query parameter: [] @@ -902,10 +847,7 @@ paths: content: application/json: schema: - type: object - properties: - success: - type: boolean + $ref: '#/components/schemas/SuccessObject' description: "Changes the permissions for a user. Available values for each permission are: `default`, `yes`, and `no`. `default` will use the user's role's default permission." security: - Auth query parameter: [] @@ -913,18 +855,7 @@ paths: content: application/json: schema: - type: object - properties: - user_uid: - type: string - permission: - type: string - new_value: - type: string - required: - - user_uid - - permission - - new_value + $ref: '#/components/schemas/ChangeUserPermissionsRequest' tags: - multi-user mode /api/changeRolePermissions: @@ -937,26 +868,12 @@ paths: content: application/json: schema: - type: object - properties: - success: - type: boolean + $ref: '#/components/schemas/SuccessObject' requestBody: content: application/json: schema: - type: object - properties: - role: - type: string - permission: - type: string - new_value: - type: string - required: - - role - - permission - - new_value + $ref: '#/components/schemas/ChangeRolePermissionsRequest' description: 'Changes the permissions for a role. Available values for each permission are: `yes`, and `no`.' security: - Auth query parameter: [] @@ -985,6 +902,13 @@ paths: - multi-user mode components: schemas: + SuccessObject: + required: + - success + type: object + properties: + success: + type: boolean FileType: type: string enum: @@ -1064,6 +988,33 @@ components: properties: videopathEncoded: type: string + GetDownloadRequest: + type: object + properties: + session_id: + type: string + download_id: + type: string + required: + - session_id + - download_id + GetDownloadResponse: + type: object + properties: + download: + $ref: '#/components/schemas/Download' + nullable: true + GetAllDownloadsResponse: + type: object + properties: + downloads: + type: object + description: Map of Session ID to inner map + additionalProperties: + type: object + description: Map of Download UID to downoad + additionalProperties: + $ref: '#/components/schemas/Download' GetMp3sResponse: required: - mp3s @@ -1370,13 +1321,6 @@ components: type: string type: type: string - body_14: - required: - - uid - type: object - properties: - uid: - type: string body_15: required: - fileNames @@ -1402,7 +1346,7 @@ components: subscriptionPlaylist: type: boolean description: Only used for subscriptions - body_16: + DeleteFileRequest: required: - fileName - type @@ -1411,7 +1355,7 @@ components: fileName: type: string type: - type: string + $ref: '#/components/schemas/FileType' apidownloadArchive_sub: required: - archive_dir @@ -1470,13 +1414,15 @@ components: new_api_key: type: string example: 4241b401-7236-493e-92b5-b72696b9d853 - body_21: + DeleteMp3Mp4Request: required: - uid type: object properties: uid: type: string + blacklistMode: + type: boolean inline_response_200_17: required: - config_file @@ -1552,6 +1498,49 @@ components: type: string thumbnailURL: type: string + Download: + required: + - uid + - ui_uid + - downloading + - complete + - url + - type + - percent_complete + - is_playlist + - timestamp_start + type: object + properties: + uid: + type: string + ui_uid: + type: string + downloading: + type: boolean + complete: + type: boolean + url: + type: string + type: + type: string + percent_complete: + type: number + is_playlist: + type: boolean + timestamp_start: + type: number + timestamp_end: + type: number + filesize: + type: number + nullable: true + error: + type: string + description: Error text, set if download fails. + fileNames: + type: array + items: + type: string inline_response_200_6_new_sub: required: - id @@ -1674,11 +1663,51 @@ components: permissions: type: array items: - type: string + $ref: '#/components/schemas/UserPermission' permission_overrides: type: array items: - type: string + $ref: '#/components/schemas/UserPermission' + UserPermission: + type: string + enum: + - filemanager + - settings + - subscriptions + - sharing + - advanced_download + - downloads_manager + BaseChangePermissionsRequest: + required: + - permission + - new_value + type: object + properties: + permission: + $ref: '#/components/schemas/UserPermission' + new_value: + type: string + enum: + - 'yes' + - 'no' + ChangeUserPermissionsRequest: + allOf: + - $ref: '#/components/schemas/BaseChangePermissionsRequest' + - type: object + properties: + user_uid: + type: string + required: + - user_uid + ChangeRolePermissionsRequest: + allOf: + - $ref: '#/components/schemas/BaseChangePermissionsRequest' + - type: object + properties: + role: + type: string + required: + - role file: title: file type: object diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 2153f73..c6784f5 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -10,7 +10,10 @@ import { DOCUMENT } from '@angular/common'; import { BehaviorSubject } from 'rxjs'; import { MatSnackBar } from '@angular/material/snack-bar'; import * as Fingerprint2 from 'fingerprintjs2'; -import type { FileType, GetAllFilesResponse, GetFileRequest, GetFileResponse, GetMp3sResponse, GetMp4sResponse, Mp3DownloadRequest, Mp3DownloadResponse, Mp4DownloadRequest, Mp4DownloadResponse } from 'api-types'; +import type { DeleteMp3Mp4Request, FileType, GetAllFilesResponse, GetFileRequest, GetFileResponse, GetMp3sResponse, GetMp4sResponse, Mp3DownloadRequest, Mp3DownloadResponse, Mp4DownloadRequest, Mp4DownloadResponse } from 'api-types'; +import { GetAllDownloadsResponse } from 'api-types/models/GetAllDownloadsResponse'; +import { GetDownloadResponse } from 'api-types/models/GetDownloadResponse'; +import { GetDownloadRequest } from 'api-types/models/GetDownloadRequest'; @Injectable() export class PostsService implements CanActivate { @@ -203,10 +206,11 @@ export class PostsService implements CanActivate { } deleteFile(uid: string, isAudio: boolean, blacklistMode = false) { + const body: DeleteMp3Mp4Request = {uid: uid, blacklistMode: blacklistMode} if (isAudio) { - return this.http.post(this.path + 'deleteMp3', {uid: uid, blacklistMode: blacklistMode}, this.httpOptions); + return this.http.post(this.path + 'deleteMp3', body, this.httpOptions); } else { - return this.http.post(this.path + 'deleteMp4', {uid: uid, blacklistMode: blacklistMode}, this.httpOptions); + return this.http.post(this.path + 'deleteMp4', body, this.httpOptions); } } @@ -330,12 +334,13 @@ export class PostsService implements CanActivate { // current downloads getCurrentDownloads() { - return this.http.get(this.path + 'downloads', this.httpOptions); + return this.http.get(this.path + 'downloads', this.httpOptions); } // current download - getCurrentDownload(session_id, download_id) { - return this.http.post(this.path + 'download', {download_id: download_id, session_id: session_id}, this.httpOptions); + getCurrentDownload(session_id: string, download_id: string) { + const body: GetDownloadRequest = {download_id: download_id, session_id: session_id}; + return this.http.post(this.path + 'download', body, this.httpOptions); } // clear downloads. download_id is optional, if it exists only 1 download will be cleared From 62c79c267e805ea16682be0aace33c640b12629d Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Thu, 24 Sep 2020 15:15:23 -0700 Subject: [PATCH 003/680] Add additional types, mainly for subscriptions --- Public API v1.yaml | 226 ++++++++++++++------------------------ src/app/posts.services.ts | 56 +++++++--- 2 files changed, 126 insertions(+), 156 deletions(-) diff --git a/Public API v1.yaml b/Public API v1.yaml index 2289c3b..589e91f 100644 --- a/Public API v1.yaml +++ b/Public API v1.yaml @@ -184,14 +184,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body_5' + $ref: '#/components/schemas/SubscribeRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_6' + $ref: '#/components/schemas/SubscribeResponse' security: - Auth query parameter: [] /api/unsubscribe: @@ -205,14 +205,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body_6' + $ref: '#/components/schemas/UnsubscribeRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_7' + $ref: '#/components/schemas/UnsubscribeResponse' security: - Auth query parameter: [] /api/deleteSubscriptionFile: @@ -226,14 +226,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body_7' + $ref: '#/components/schemas/DeleteSubscriptionFileRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_8' + $ref: '#/components/schemas/SuccessObject' '500': description: Internal Server Error security: @@ -249,14 +249,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body_8' + $ref: '#/components/schemas/GetSubscriptionRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_9' + $ref: '#/components/schemas/GetSubscriptionResponse' security: - Auth query parameter: [] /api/downloadVideosForSubscription: @@ -270,14 +270,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body_9' + $ref: '#/components/schemas/DownloadVideosForSubscriptionRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_10' + $ref: '#/components/schemas/SuccessObject' security: - Auth query parameter: [] /api/getAllSubscriptions: @@ -297,7 +297,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_11' + $ref: '#/components/schemas/GetAllSubscriptionsResponse' security: - Auth query parameter: [] /api/createPlaylist: @@ -360,7 +360,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_5' + $ref: '#/components/schemas/SuccessObject' security: - Auth query parameter: [] /api/deletePlaylist: @@ -457,6 +457,8 @@ paths: responses: '200': description: The archive text file is sent as a response + '404': + description: If the archive dir is not found, 404 is sent as a response security: - Auth query parameter: [] /api/updaterStatus: @@ -1094,16 +1096,11 @@ components: $ref: '#/components/schemas/FileType' is_playlist: type: boolean - inline_response_200_5: - required: - - success - type: object - properties: - success: - type: boolean - body_5: + SubscribeRequest: required: + - name - url + - streamingOnly type: object properties: name: @@ -1114,46 +1111,32 @@ components: type: string streamingOnly: type: boolean - inline_response_200_6: + audioOnly: + type: boolean + customArgs: + type: string + customFileOutput: + type: string + SubscribeResponse: required: - new_sub type: object properties: new_sub: - $ref: '#/components/schemas/inline_response_200_6_new_sub' + $ref: '#/components/schemas/Subscription' error: type: string - apiunsubscribe_sub: - required: - - id - - name - - url - - videos - type: object - properties: - name: - type: string - url: - type: string - id: - type: string - streamingOnly: - type: boolean - videos: - type: array - items: - type: object - body_6: + UnsubscribeRequest: required: - sub type: object properties: sub: - $ref: '#/components/schemas/apiunsubscribe_sub' + $ref: '#/components/schemas/SubscriptionRequestData' deleteMode: type: boolean description: Defaults to false - inline_response_200_7: + UnsubscribeResponse: required: - success type: object @@ -1162,30 +1145,7 @@ components: type: boolean error: type: string - apideleteSubscriptionFile_sub: - required: - - id - - isPlaylist - - name - - url - - videos - type: object - properties: - name: - type: string - url: - type: string - id: - type: string - streamingOnly: - type: boolean - isPlaylist: - type: boolean - videos: - type: array - items: - type: object - body_7: + DeleteSubscriptionFileRequest: required: - file - sub @@ -1193,50 +1153,41 @@ components: properties: file: type: string + file_uid: + type: string sub: - $ref: '#/components/schemas/apideleteSubscriptionFile_sub' + $ref: '#/components/schemas/SubscriptionRequestData' deleteForever: type: boolean description: 'If true, does not remove id from archive. Only valid if youtube-dl archive is enabled in settings.' - inline_response_200_8: - type: object - properties: - success: - type: boolean - body_8: + GetSubscriptionRequest: required: - - subID + - id type: object properties: - subID: + id: type: string - inline_response_200_9: + description: Subscription ID + GetSubscriptionResponse: required: - files - subscription type: object properties: subscription: - $ref: '#/components/schemas/inline_response_200_9_subscription' + $ref: '#/components/schemas/Subscription' files: type: array items: type: object - body_9: + DownloadVideosForSubscriptionRequest: required: - subID type: object properties: subID: type: string - inline_response_200_10: - required: - - success - type: object - properties: - success: - type: number - inline_response_200_11: + GetAllSubscriptionsResponse: required: - subscriptions type: object @@ -1244,7 +1195,7 @@ components: subscriptions: type: array items: - $ref: '#/components/schemas/inline_response_200_11_subscriptions' + $ref: '#/components/schemas/Subscription' body_10: required: - fileNames @@ -1356,20 +1307,18 @@ components: type: string type: $ref: '#/components/schemas/FileType' - apidownloadArchive_sub: - required: - - archive_dir - type: object - properties: - archive_dir: - type: string - body_17: + DownloadArchiveRequest: required: - sub type: object properties: sub: - $ref: '#/components/schemas/apidownloadArchive_sub' + required: + - archive_dir + type: object + properties: + archive_dir: + type: string inline_response_200_14: required: - details @@ -1541,11 +1490,32 @@ components: type: array items: type: string - inline_response_200_6_new_sub: + SubscriptionRequestData: + required: + - id + - name + type: object + properties: + name: + type: string + id: + type: string + type: + $ref: '#/components/schemas/FileType' + isPlaylist: + type: boolean + archive: + type: string + Subscription: required: - id - name - url + - type + - user_uid + - streamingOnly + - isPlaylist + - videos type: object properties: name: @@ -1554,53 +1524,27 @@ components: type: string id: type: string + type: + $ref: '#/components/schemas/FileType' + user_uid: + type: string + nullable: true streamingOnly: type: boolean + isPlaylist: + type: boolean + archive: + type: string timerange: type: string - inline_response_200_9_subscription: - required: - - archive - - id - - isPlaylist - - name - - url - type: object - properties: - name: + custom_args: type: string - url: - type: string - id: - type: string - streamingOnly: - type: boolean - isPlaylist: - type: boolean - archive: - type: string - inline_response_200_11_subscriptions: - required: - - archive - - id - - isPlaylist - - name - - streamingOnly - - url - type: object - properties: - name: - type: string - url: - type: string - id: - type: string - streamingOnly: - type: boolean - isPlaylist: - type: boolean - archive: + custom_output: type: string + videos: + type: array + items: + type: object inline_response_200_12_new_playlist: required: - fileNames diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index c6784f5..92b2b43 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -10,10 +10,32 @@ import { DOCUMENT } from '@angular/common'; import { BehaviorSubject } from 'rxjs'; import { MatSnackBar } from '@angular/material/snack-bar'; import * as Fingerprint2 from 'fingerprintjs2'; -import type { DeleteMp3Mp4Request, FileType, GetAllFilesResponse, GetFileRequest, GetFileResponse, GetMp3sResponse, GetMp4sResponse, Mp3DownloadRequest, Mp3DownloadResponse, Mp4DownloadRequest, Mp4DownloadResponse } from 'api-types'; -import { GetAllDownloadsResponse } from 'api-types/models/GetAllDownloadsResponse'; -import { GetDownloadResponse } from 'api-types/models/GetDownloadResponse'; -import { GetDownloadRequest } from 'api-types/models/GetDownloadRequest'; +import { + DeleteMp3Mp4Request, + DeleteSubscriptionFileRequest, + FileType, + GetAllDownloadsResponse, + GetAllFilesResponse, + GetAllSubscriptionsResponse, + GetDownloadResponse, + GetDownloadRequest, + GetFileRequest, + GetFileResponse, + GetMp3sResponse, + GetMp4sResponse, + GetSubscriptionRequest, + GetSubscriptionResponse, + Mp3DownloadRequest, + Mp3DownloadResponse, + Mp4DownloadRequest, + Mp4DownloadResponse, + SubscribeRequest, + SubscribeResponse, + SubscriptionRequestData, + SuccessObject, + UnsubscribeRequest, + UnsubscribeResponse +} from 'api-types'; @Injectable() export class PostsService implements CanActivate { @@ -306,30 +328,34 @@ export class PostsService implements CanActivate { return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type}, this.httpOptions); } - createSubscription(url, name, timerange = null, streamingOnly = false, audioOnly = false, customArgs = null, customFileOutput = null) { - return this.http.post(this.path + 'subscribe', {url: url, name: name, timerange: timerange, streamingOnly: streamingOnly, - audioOnly: audioOnly, customArgs: customArgs, customFileOutput: customFileOutput}, this.httpOptions); + createSubscription(url: string, name: string, timerange: string = null, streamingOnly = false, audioOnly = false, customArgs: string = null, customFileOutput: string = null) { + const body: SubscribeRequest = {url: url, name: name, timerange: timerange, streamingOnly: streamingOnly, + audioOnly: audioOnly, customArgs: customArgs, customFileOutput: customFileOutput}; + return this.http.post(this.path + 'subscribe', body, this.httpOptions); } updateSubscription(subscription) { return this.http.post(this.path + 'updateSubscription', {subscription: subscription}, this.httpOptions); } - unsubscribe(sub, deleteMode = false) { - return this.http.post(this.path + 'unsubscribe', {sub: sub, deleteMode: deleteMode}, this.httpOptions) + unsubscribe(sub: SubscriptionRequestData, deleteMode = false) { + const body: UnsubscribeRequest = {sub: sub, deleteMode: deleteMode}; + return this.http.post(this.path + 'unsubscribe', body, this.httpOptions) } - deleteSubscriptionFile(sub, file, deleteForever, file_uid) { - return this.http.post(this.path + 'deleteSubscriptionFile', {sub: sub, file: file, deleteForever: deleteForever, - file_uid: file_uid}, this.httpOptions) + deleteSubscriptionFile(sub: SubscriptionRequestData, file: string, deleteForever: boolean, file_uid: string) { + const body: DeleteSubscriptionFileRequest = {sub: sub, file: file, deleteForever: deleteForever, + file_uid: file_uid}; + return this.http.post(this.path + 'deleteSubscriptionFile', body, this.httpOptions) } - getSubscription(id) { - return this.http.post(this.path + 'getSubscription', {id: id}, this.httpOptions); + getSubscription(id: string) { + const body: GetSubscriptionRequest = {id: id}; + return this.http.post(this.path + 'getSubscription', body, this.httpOptions); } getAllSubscriptions() { - return this.http.post(this.path + 'getAllSubscriptions', {}, this.httpOptions); + return this.http.post(this.path + 'getAllSubscriptions', {}, this.httpOptions); } // current downloads From 4d74c375f4c5ad188b28e3ae243abab158987244 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Thu, 24 Sep 2020 15:39:37 -0700 Subject: [PATCH 004/680] Add playlist types --- Public API v1.yaml | 114 +++++++++++++++++------------ src/app/main/main.component.ts | 5 +- src/app/player/player.component.ts | 5 +- src/app/posts.services.ts | 60 +++++++++------ 4 files changed, 112 insertions(+), 72 deletions(-) diff --git a/Public API v1.yaml b/Public API v1.yaml index 589e91f..96ba54d 100644 --- a/Public API v1.yaml +++ b/Public API v1.yaml @@ -148,7 +148,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_5' + $ref: '#/components/schemas/SuccessObject' security: - Auth query parameter: [] /api/disableSharing: @@ -170,7 +170,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_5' + $ref: '#/components/schemas/SuccessObject' security: - Auth query parameter: [] /api/subscribe: @@ -311,14 +311,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body_10' + $ref: '#/components/schemas/CreatePlaylistRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_12' + $ref: '#/components/schemas/CreatePlaylistResponse' security: - Auth query parameter: [] /api/getPlaylist: @@ -332,28 +332,49 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body_11' + $ref: '#/components/schemas/GetPlaylistRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_13' + $ref: '#/components/schemas/GetPlaylistResponse' security: - Auth query parameter: [] /api/updatePlaylist: post: tags: - playlists - summary: Update playlist files - description: Updates the list of filenames in the playlist object + summary: Update playlist + description: Updates the playlist object operationId: post-api-updatePlaylist requestBody: content: application/json: schema: - $ref: '#/components/schemas/body_12' + $ref: '#/components/schemas/UpdatePlaylistRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessObject' + security: + - Auth query parameter: [] + /api/updatePlaylistFiles: + post: + tags: + - playlists + summary: Update playlist files + description: Updates the list of filenames in the playlist object + operationId: post-api-updatePlaylistFiles + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdatePlaylistFilesRequest' responses: '200': description: OK @@ -374,14 +395,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body_13' + $ref: '#/components/schemas/DeletePlaylistRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_5' + $ref: '#/components/schemas/SuccessObject' security: - Auth query parameter: [] /api/deleteMp4: @@ -495,7 +516,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_5' + $ref: '#/components/schemas/SuccessObject' security: - Auth query parameter: [] /api/isPinSet: @@ -532,7 +553,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_5' + $ref: '#/components/schemas/SuccessObject' security: - Auth query parameter: [] /api/setPin: @@ -552,7 +573,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_5' + $ref: '#/components/schemas/SuccessObject' security: - Auth query parameter: [] description: '' @@ -625,7 +646,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_5' + $ref: '#/components/schemas/SuccessObject' security: - Auth query parameter: [] /api/downloads: @@ -1196,12 +1217,13 @@ components: type: array items: $ref: '#/components/schemas/Subscription' - body_10: + CreatePlaylistRequest: required: - fileNames - playlistName - thumbnailURL - type + - duration type: object properties: playlistName: @@ -1211,30 +1233,33 @@ components: items: type: string type: - type: string + $ref: '#/components/schemas/FileType' thumbnailURL: type: string - inline_response_200_12: + duration: + type: number + CreatePlaylistResponse: required: - new_playlist - success type: object properties: new_playlist: - $ref: '#/components/schemas/inline_response_200_12_new_playlist' + $ref: '#/components/schemas/Playlist' success: type: boolean - body_11: + GetPlaylistRequest: required: - playlistID - - type type: object properties: playlistID: type: string type: + $ref: '#/components/schemas/FileType' + uuid: type: string - inline_response_200_13: + GetPlaylistResponse: required: - playlist - success @@ -1244,10 +1269,17 @@ components: playlist: $ref: '#/components/schemas/Playlist' type: - type: string + $ref: '#/components/schemas/FileType' success: type: boolean - body_12: + UpdatePlaylistRequest: + required: + - playlist + type: object + properties: + playlist: + $ref: '#/components/schemas/Playlist' + UpdatePlaylistFilesRequest: required: - fileNames - playlistID @@ -1261,8 +1293,8 @@ components: items: type: string type: - type: string - body_13: + $ref: '#/components/schemas/FileType' + DeletePlaylistRequest: required: - playlistID - type @@ -1271,7 +1303,7 @@ components: playlistID: type: string type: - type: string + $ref: '#/components/schemas/FileType' body_15: required: - fileNames @@ -1435,6 +1467,9 @@ components: - id - name - thumbnailURL + - type + - registered + - duration type: object properties: name: @@ -1447,6 +1482,12 @@ components: type: string thumbnailURL: type: string + type: + $ref: '#/components/schemas/FileType' + registered: + type: number + duration: + type: number Download: required: - uid @@ -1545,25 +1586,6 @@ components: type: array items: type: object - inline_response_200_12_new_playlist: - required: - - fileNames - - id - - name - - thumbnailURL - - type - type: object - properties: - name: - type: string - fileNames: - type: string - id: - type: string - thumbnailURL: - type: string - type: - type: string User: title: user type: object diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts index 3625449..28e672c 100644 --- a/src/app/main/main.component.ts +++ b/src/app/main/main.component.ts @@ -20,6 +20,7 @@ import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.com import { Platform } from '@angular/cdk/platform'; import { v4 as uuid } from 'uuid'; import { ArgModifierDialogComponent } from 'app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component'; +import type { FileType } from 'api-types'; export let audioFilesMouseHovering = false; export let videoFilesMouseHovering = false; @@ -453,7 +454,7 @@ export class MainComponent implements OnInit { } public removePlaylistMp3(playlistID, index) { - this.postsService.removePlaylist(playlistID, 'audio').subscribe(res => { + this.postsService.removePlaylist(playlistID, 'audio' as FileType).subscribe(res => { if (res['success']) { this.playlists.audio.splice(index, 1); this.openSnackBar('Playlist successfully removed.', ''); @@ -472,7 +473,7 @@ export class MainComponent implements OnInit { } public removePlaylistMp4(playlistID, index) { - this.postsService.removePlaylist(playlistID, 'video').subscribe(res => { + this.postsService.removePlaylist(playlistID, 'video' as FileType).subscribe(res => { if (res['success']) { this.playlists.video.splice(index, 1); this.openSnackBar('Playlist successfully removed.', ''); diff --git a/src/app/player/player.component.ts b/src/app/player/player.component.ts index 3640d7ad..79eb371 100644 --- a/src/app/player/player.component.ts +++ b/src/app/player/player.component.ts @@ -7,6 +7,7 @@ import { MatSnackBar } from '@angular/material/snack-bar'; import { InputDialogComponent } from 'app/input-dialog/input-dialog.component'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { ShareMediaDialogComponent } from '../dialogs/share-media-dialog/share-media-dialog.component'; +import type { FileType } from 'api-types'; export interface IMedia { title: string; @@ -360,7 +361,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { // Eventually do additional checks on name if (name) { const fileNames = this.getFileNames(); - this.postsService.createPlaylist(name, fileNames, this.type, null).subscribe(res => { + this.postsService.createPlaylist(name, fileNames, this.type as FileType, null).subscribe(res => { if (res['success']) { dialogRef.close(); const new_playlist = res['new_playlist']; @@ -401,7 +402,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { updatePlaylist() { const fileNames = this.getFileNames(); this.playlist_updating = true; - this.postsService.updatePlaylistFiles(this.id, fileNames, this.type).subscribe(res => { + this.postsService.updatePlaylistFiles(this.id, fileNames, this.type as FileType).subscribe(res => { this.playlist_updating = false; if (res['success']) { const fileNamesEncoded = fileNames.join('|nvr|'); diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 92b2b43..c7e7322 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -11,7 +11,10 @@ import { BehaviorSubject } from 'rxjs'; import { MatSnackBar } from '@angular/material/snack-bar'; import * as Fingerprint2 from 'fingerprintjs2'; import { + CreatePlaylistRequest, + CreatePlaylistResponse, DeleteMp3Mp4Request, + DeletePlaylistRequest, DeleteSubscriptionFileRequest, FileType, GetAllDownloadsResponse, @@ -23,18 +26,24 @@ import { GetFileResponse, GetMp3sResponse, GetMp4sResponse, + GetPlaylistRequest, + GetPlaylistResponse, GetSubscriptionRequest, GetSubscriptionResponse, Mp3DownloadRequest, Mp3DownloadResponse, Mp4DownloadRequest, Mp4DownloadResponse, + Playlist, + SharingToggle, SubscribeRequest, SubscribeResponse, SubscriptionRequestData, SuccessObject, UnsubscribeRequest, - UnsubscribeResponse + UnsubscribeResponse, + UpdatePlaylistFilesRequest, + UpdatePlaylistRequest, } from 'api-types'; @Injectable() @@ -293,39 +302,46 @@ export class PostsService implements CanActivate { return this.http.post(this.path + 'generateNewAPIKey', {}, this.httpOptions); } - enableSharing(uid, type, is_playlist) { - return this.http.post(this.path + 'enableSharing', {uid: uid, type: type, is_playlist: is_playlist}, this.httpOptions); + enableSharing(uid: string, type: FileType, is_playlist: boolean) { + const body: SharingToggle = {uid: uid, type: type, is_playlist: is_playlist}; + return this.http.post(this.path + 'enableSharing', body, this.httpOptions); } - disableSharing(uid, type, is_playlist) { - return this.http.post(this.path + 'disableSharing', {uid: uid, type: type, is_playlist: is_playlist}, this.httpOptions); + disableSharing(uid: string, type: FileType, is_playlist: boolean) { + const body: SharingToggle = {uid: uid, type: type, is_playlist: is_playlist}; + return this.http.post(this.path + 'disableSharing', body, this.httpOptions); } - createPlaylist(playlistName, fileNames, type, thumbnailURL, duration = null) { - return this.http.post(this.path + 'createPlaylist', {playlistName: playlistName, - fileNames: fileNames, - type: type, - thumbnailURL: thumbnailURL, - duration: duration}, this.httpOptions); + createPlaylist(playlistName: string, fileNames: string[], type: FileType, thumbnailURL: string, duration: number = null) { + const body: CreatePlaylistRequest = {playlistName: playlistName, + fileNames: fileNames, + type: type, + thumbnailURL: thumbnailURL, + duration: duration}; + return this.http.post(this.path + 'createPlaylist', body, this.httpOptions); } - getPlaylist(playlistID, type, uuid = null) { - return this.http.post(this.path + 'getPlaylist', {playlistID: playlistID, - type: type, uuid: uuid}, this.httpOptions); + getPlaylist(playlistID: string, type: FileType, uuid: string = null) { + const body: GetPlaylistRequest = {playlistID: playlistID, + type: type, uuid: uuid}; + return this.http.post(this.path + 'getPlaylist', body, this.httpOptions); } - updatePlaylist(playlist) { - return this.http.post(this.path + 'updatePlaylist', {playlist: playlist}, this.httpOptions); + updatePlaylist(playlist: Playlist) { + const body: UpdatePlaylistRequest = {playlist: playlist}; + return this.http.post(this.path + 'updatePlaylist', body, this.httpOptions); } - updatePlaylistFiles(playlistID, fileNames, type) { - return this.http.post(this.path + 'updatePlaylistFiles', {playlistID: playlistID, - fileNames: fileNames, - type: type}, this.httpOptions); + updatePlaylistFiles(playlistID: string, fileNames: string[], type: FileType) { + const body: UpdatePlaylistFilesRequest = {playlistID: playlistID, + fileNames: fileNames, + type: type}; + return this.http.post(this.path + 'updatePlaylistFiles', body, this.httpOptions); } - removePlaylist(playlistID, type) { - return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type}, this.httpOptions); + removePlaylist(playlistID: string, type: FileType) { + const body: DeletePlaylistRequest = {playlistID: playlistID, type: type}; + return this.http.post(this.path + 'deletePlaylist', body, this.httpOptions); } createSubscription(url: string, name: string, timerange: string = null, streamingOnly = false, audioOnly = false, customArgs: string = null, customFileOutput: string = null) { From fe7a3075d6dca48058518815e1f37b7fa216e237 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Fri, 25 Sep 2020 10:26:05 -0700 Subject: [PATCH 005/680] Rename last few active API routes --- Public API v1.yaml | 75 +++++++++++-------- .../recent-videos/recent-videos.component.ts | 9 ++- .../update-progress-dialog.component.ts | 3 +- src/app/main/main.component.ts | 8 +- src/app/player/player.component.ts | 8 +- src/app/posts.services.ts | 72 +++++++++++------- .../subscription/subscription.component.ts | 3 +- 7 files changed, 106 insertions(+), 72 deletions(-) diff --git a/Public API v1.yaml b/Public API v1.yaml index 96ba54d..e508fd5 100644 --- a/Public API v1.yaml +++ b/Public API v1.yaml @@ -437,11 +437,7 @@ paths: content: application/json: schema: - type: object - properties: {} - application/xml: - schema: - $ref: '#/components/schemas/body_15' + $ref: '#/components/schemas/DownloadFileRequest' responses: '200': description: 'The file itself is in the response, as well as an options object.' @@ -474,7 +470,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body_17' + $ref: '#/components/schemas/DownloadArchiveRequest' responses: '200': description: The archive text file is sent as a response @@ -495,7 +491,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_14' + $ref: '#/components/schemas/UpdaterStatus' security: - Auth query parameter: [] /api/updateServer: @@ -509,7 +505,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body_18' + $ref: '#/components/schemas/UpdateServerRequest' responses: '200': description: OK @@ -590,7 +586,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_16' + $ref: '#/components/schemas/GenerateNewApiKeyResponse' security: - Auth query parameter: [] /api/deleteMp3: @@ -627,7 +623,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/inline_response_200_17' + $ref: '#/components/schemas/ConfigResponse' security: - Auth query parameter: [] /api/setConfig: @@ -639,7 +635,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body_22' + $ref: '#/components/schemas/SetConfigRequest' responses: '200': description: OK @@ -647,6 +643,8 @@ paths: application/json: schema: $ref: '#/components/schemas/SuccessObject' + '404': + description: Tried to save invalid config file. security: - Auth query parameter: [] /api/downloads: @@ -937,6 +935,13 @@ components: enum: - audio - video + Config: + required: + - YoutubeDLMaterial + type: object + properties: + YoutubeDLMaterial: + type: object BaseDownloadRequest: required: - url @@ -1304,29 +1309,33 @@ components: type: string type: $ref: '#/components/schemas/FileType' - body_15: + DownloadFileRequest: required: - fileNames - type type: object properties: fileNames: - type: array - description: Array of 1 or more files to download - items: - type: string + oneOf: + - type: string + - type: array + description: Array of 1 or more files to download + items: + type: string zip_mode: type: boolean type: - type: string + $ref: '#/components/schemas/FileType' outputName: type: string fullPathProvided: + type: boolean + uuid: type: string subscriptionName: type: boolean description: Only used for subscriptions - subscriptionPlaylist: + subPlaylist: type: boolean description: Only used for subscriptions DeleteFileRequest: @@ -1351,7 +1360,7 @@ components: properties: archive_dir: type: string - inline_response_200_14: + UpdaterStatus: required: - details - updating @@ -1361,7 +1370,11 @@ components: type: boolean details: type: string - body_18: + error: + type: boolean + UpdateServerRequest: + required: + - tag type: object properties: tag: @@ -1387,7 +1400,7 @@ components: properties: unhashed_pin: type: string - inline_response_200_16: + GenerateNewApiKeyResponse: required: - new_api_key type: object @@ -1404,23 +1417,23 @@ components: type: string blacklistMode: type: boolean - inline_response_200_17: + ConfigResponse: required: - config_file - success type: object properties: config_file: - type: object + $ref: '#/components/schemas/Config' success: type: boolean - body_22: + SetConfigRequest: required: - new_config_file type: object properties: new_config_file: - type: object + $ref: '#/components/schemas/Config' DatabaseFile: required: - duration @@ -1621,7 +1634,7 @@ components: subscriptions: type: array items: - $ref: '#/components/schemas/inline_response_200_9_subscription' + $ref: '#/components/schemas/Subscription' created: type: number role: @@ -1643,6 +1656,11 @@ components: - sharing - advanced_download - downloads_manager + YesNo: + type: string + enum: + - 'yes' + - 'no' BaseChangePermissionsRequest: required: - permission @@ -1652,10 +1670,7 @@ components: permission: $ref: '#/components/schemas/UserPermission' new_value: - type: string - enum: - - 'yes' - - 'no' + $ref: '#/components/schemas/YesNo' ChangeUserPermissionsRequest: allOf: - $ref: '#/components/schemas/BaseChangePermissionsRequest' diff --git a/src/app/components/recent-videos/recent-videos.component.ts b/src/app/components/recent-videos/recent-videos.component.ts index 8333f79..11a6dcc 100644 --- a/src/app/components/recent-videos/recent-videos.component.ts +++ b/src/app/components/recent-videos/recent-videos.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { PostsService } from 'app/posts.services'; import { Router } from '@angular/router'; +import type { FileType } from '../../../api-types'; @Component({ selector: 'app-recent-videos', @@ -161,7 +162,7 @@ export class RecentVideosComponent implements OnInit { // normal subscriptions !new_tab ? this.router.navigate(['/player', {fileNames: file.id, type: file.isAudio ? 'audio' : 'video', subscriptionName: sub.name, - subPlaylist: sub.isPlaylist}]) + subPlaylist: sub.isPlaylist}]) : window.open(`/#/player;fileNames=${file.id};type=${file.isAudio ? 'audio' : 'video'};subscriptionName=${sub.name};subPlaylist=${sub.isPlaylist}`); } } else { @@ -186,7 +187,7 @@ export class RecentVideosComponent implements OnInit { } downloadSubscriptionFile(file) { - const type = file.isAudio ? 'audio' : 'video'; + const type = (file.isAudio ? 'audio' : 'video') as FileType; const ext = type === 'audio' ? '.mp3' : '.mp4' const sub = this.postsService.getSubscriptionByID(file.sub_id); this.postsService.downloadFileFromServer(file.id, type, null, null, sub.name, sub.isPlaylist, @@ -199,7 +200,7 @@ export class RecentVideosComponent implements OnInit { } downloadNormalFile(file) { - const type = file.isAudio ? 'audio' : 'video'; + const type = (file.isAudio ? 'audio' : 'video') as FileType; const ext = type === 'audio' ? '.mp3' : '.mp4' const name = file.id; this.downloading_content[type][name] = true; @@ -276,7 +277,7 @@ export class RecentVideosComponent implements OnInit { const result = b.registered - a.registered; return result; } - + durationStringToNumber(dur_str) { let num_sum = 0; const dur_str_parts = dur_str.split(':'); diff --git a/src/app/dialogs/update-progress-dialog/update-progress-dialog.component.ts b/src/app/dialogs/update-progress-dialog/update-progress-dialog.component.ts index fe1fbf4..9ca066f 100644 --- a/src/app/dialogs/update-progress-dialog/update-progress-dialog.component.ts +++ b/src/app/dialogs/update-progress-dialog/update-progress-dialog.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { PostsService } from 'app/posts.services'; import { MatSnackBar } from '@angular/material/snack-bar'; +import type { UpdaterStatus } from '../../../api-types'; @Component({ selector: 'app-update-progress-dialog', @@ -9,7 +10,7 @@ import { MatSnackBar } from '@angular/material/snack-bar'; }) export class UpdateProgressDialogComponent implements OnInit { - updateStatus = null; + updateStatus: UpdaterStatus = null; updateInterval = 250; errored = false; diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts index 28e672c..c0bb894 100644 --- a/src/app/main/main.component.ts +++ b/src/app/main/main.component.ts @@ -20,7 +20,7 @@ import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.com import { Platform } from '@angular/cdk/platform'; import { v4 as uuid } from 'uuid'; import { ArgModifierDialogComponent } from 'app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component'; -import type { FileType } from 'api-types'; +import type { FileType } from '../../api-types'; export let audioFilesMouseHovering = false; export let videoFilesMouseHovering = false; @@ -34,7 +34,7 @@ export interface Download { percent_complete: number; downloading: boolean; is_playlist: boolean; - error: boolean | string; + error?: boolean | string; fileNames?: string[]; complete?: boolean; timestamp_start?: number; @@ -740,7 +740,7 @@ export class MainComponent implements OnInit { downloadAudioFile(name) { this.downloading_content['audio'][name] = true; - this.postsService.downloadFileFromServer(name, 'audio').subscribe(res => { + this.postsService.downloadFileFromServer(name, 'audio' as FileType).subscribe(res => { this.downloading_content['audio'][name] = false; const blob: Blob = res; saveAs(blob, decodeURIComponent(name) + '.mp3'); @@ -757,7 +757,7 @@ export class MainComponent implements OnInit { downloadVideoFile(name) { this.downloading_content['video'][name] = true; - this.postsService.downloadFileFromServer(name, 'video').subscribe(res => { + this.postsService.downloadFileFromServer(name, 'video' as FileType).subscribe(res => { this.downloading_content['video'][name] = false; const blob: Blob = res; saveAs(blob, decodeURIComponent(name) + '.mp4'); diff --git a/src/app/player/player.component.ts b/src/app/player/player.component.ts index 79eb371..57e2ea4 100644 --- a/src/app/player/player.component.ts +++ b/src/app/player/player.component.ts @@ -7,7 +7,7 @@ import { MatSnackBar } from '@angular/material/snack-bar'; import { InputDialogComponent } from 'app/input-dialog/input-dialog.component'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { ShareMediaDialogComponent } from '../dialogs/share-media-dialog/share-media-dialog.component'; -import type { FileType } from 'api-types'; +import type { FileType } from '../../api-types'; export interface IMedia { title: string; @@ -35,7 +35,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { // params fileNames: string[]; - type: string; + type: FileType; id = null; // used for playlists (not subscription) uid = null; // used for non-subscription files (audio, video, playlist) subscriptionName = null; @@ -74,7 +74,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { ngOnInit(): void { this.innerWidth = window.innerWidth; - this.type = this.route.snapshot.paramMap.get('type'); + this.type = this.route.snapshot.paramMap.get('type') as FileType; this.id = this.route.snapshot.paramMap.get('id'); this.uid = this.route.snapshot.paramMap.get('uid'); this.subscriptionName = this.route.snapshot.paramMap.get('subscriptionName'); @@ -160,7 +160,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { if (!this.id) { // regular video/audio file (not playlist) this.fileNames = [this.db_file['id']]; - this.type = this.db_file['isAudio'] ? 'audio' : 'video'; + this.type = (this.db_file['isAudio'] ? 'audio' : 'video') as FileType; if (!already_has_filenames) { this.parseFileNames(); } } } diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index c7e7322..d0fad49 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -10,13 +10,19 @@ import { DOCUMENT } from '@angular/common'; import { BehaviorSubject } from 'rxjs'; import { MatSnackBar } from '@angular/material/snack-bar'; import * as Fingerprint2 from 'fingerprintjs2'; -import { +import type { + ChangeRolePermissionsRequest, + ChangeUserPermissionsRequest, + ConfigResponse, CreatePlaylistRequest, CreatePlaylistResponse, DeleteMp3Mp4Request, DeletePlaylistRequest, DeleteSubscriptionFileRequest, + DownloadArchiveRequest, + DownloadFileRequest, FileType, + GenerateNewApiKeyResponse, GetAllDownloadsResponse, GetAllFilesResponse, GetAllSubscriptionsResponse, @@ -35,16 +41,21 @@ import { Mp4DownloadRequest, Mp4DownloadResponse, Playlist, + SetConfigRequest, SharingToggle, SubscribeRequest, SubscribeResponse, SubscriptionRequestData, SuccessObject, + UpdaterStatus, UnsubscribeRequest, UnsubscribeResponse, UpdatePlaylistFilesRequest, UpdatePlaylistRequest, -} from 'api-types'; + UpdateServerRequest, + UserPermission, + YesNo, +} from '../api-types'; @Injectable() export class PostsService implements CanActivate { @@ -224,7 +235,7 @@ export class PostsService implements CanActivate { if (isDevMode()) { return this.http.get('./assets/default.json'); } else { - return this.http.get(this.path + 'config', this.httpOptions); + return this.http.get(this.path + 'config', this.httpOptions); } } @@ -233,7 +244,8 @@ export class PostsService implements CanActivate { } setConfig(config) { - return this.http.post(this.path + 'setConfig', {new_config_file: config}, this.httpOptions); + const body: SetConfigRequest = {new_config_file: config}; + return this.http.post(this.path + 'setConfig', body, this.httpOptions); } deleteFile(uid: string, isAudio: boolean, blacklistMode = false) { @@ -262,19 +274,19 @@ export class PostsService implements CanActivate { return this.http.post(this.path + 'getAllFiles', {}, this.httpOptions); } - downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null, subscriptionName = null, subPlaylist = null, - uid = null, uuid = null, id = null) { - return this.http.post(this.path + 'downloadFile', {fileNames: fileName, - type: type, - zip_mode: Array.isArray(fileName), - outputName: outputName, - fullPathProvided: fullPathProvided, - subscriptionName: subscriptionName, - subPlaylist: subPlaylist, - uuid: uuid, - uid: uid, - id: id - }, + downloadFileFromServer(fileName: string | string[], type: FileType, outputName: string = null, fullPathProvided: boolean = null, subscriptionName: boolean = null, subPlaylist: boolean = null, + uid = null, uuid: string = null, id = null) { + const body: DownloadFileRequest = {fileNames: fileName, + type: type, + zip_mode: Array.isArray(fileName), + outputName: outputName, + fullPathProvided: fullPathProvided, + subscriptionName: subscriptionName, + subPlaylist: subPlaylist, + uuid: uuid, + id: id, + }; + return this.http.post(this.path + 'downloadFile', body, {responseType: 'blob', params: this.httpOptions.params}); } @@ -283,7 +295,8 @@ export class PostsService implements CanActivate { } downloadArchive(sub) { - return this.http.post(this.path + 'downloadArchive', {sub: sub}, {responseType: 'blob', params: this.httpOptions.params}); + const body: DownloadArchiveRequest = {sub: sub}; + return this.http.post(this.path + 'downloadArchive', body, {responseType: 'blob', params: this.httpOptions.params}); } getFileInfo(fileNames, type, urlMode) { @@ -299,7 +312,7 @@ export class PostsService implements CanActivate { } generateNewAPIKey() { - return this.http.post(this.path + 'generateNewAPIKey', {}, this.httpOptions); + return this.http.post(this.path + 'generateNewAPIKey', {}, this.httpOptions); } enableSharing(uid: string, type: FileType, is_playlist: boolean) { @@ -393,12 +406,13 @@ export class PostsService implements CanActivate { } // updates the server to the latest version - updateServer(tag) { - return this.http.post(this.path + 'updateServer', {tag: tag}, this.httpOptions); + updateServer(tag: string) { + const body: UpdateServerRequest = {tag: tag}; + return this.http.post(this.path + 'updateServer', body, this.httpOptions); } getUpdaterStatus() { - return this.http.get(this.path + 'updaterStatus', this.httpOptions); + return this.http.get(this.path + 'updaterStatus', this.httpOptions); } // gets tag of the latest version of youtubedl-material @@ -534,11 +548,11 @@ export class PostsService implements CanActivate { } changeUser(change_obj) { - return this.http.post(this.path + 'updateUser', {change_object: change_obj}, this.httpOptions); + return this.http.post(this.path + 'updateUser', {change_object: change_obj}, this.httpOptions); } deleteUser(uid) { - return this.http.post(this.path + 'deleteUser', {uid: uid}, this.httpOptions); + return this.http.post(this.path + 'deleteUser', {uid: uid}, this.httpOptions); } changeUserPassword(user_uid, new_password) { @@ -553,13 +567,15 @@ export class PostsService implements CanActivate { return this.http.post(this.path + 'getRoles', {}, this.httpOptions); } - setUserPermission(user_uid, permission, new_value) { - return this.http.post(this.path + 'changeUserPermissions', {user_uid: user_uid, permission: permission, new_value: new_value}, + setUserPermission(user_uid: string, permission: UserPermission, new_value: YesNo) { + const body: ChangeUserPermissionsRequest = {user_uid: user_uid, permission: permission, new_value: new_value}; + return this.http.post(this.path + 'changeUserPermissions', body, this.httpOptions); } - setRolePermission(role_name, permission, new_value) { - return this.http.post(this.path + 'changeRolePermissions', {role: role_name, permission: permission, new_value: new_value}, + setRolePermission(role_name: string, permission: UserPermission, new_value: YesNo) { + const body: ChangeRolePermissionsRequest = {role: role_name, permission: permission, new_value: new_value}; + return this.http.post(this.path + 'changeRolePermissions', body, this.httpOptions); } diff --git a/src/app/subscription/subscription/subscription.component.ts b/src/app/subscription/subscription/subscription.component.ts index 794a268..eb2f35b 100644 --- a/src/app/subscription/subscription/subscription.component.ts +++ b/src/app/subscription/subscription/subscription.component.ts @@ -3,6 +3,7 @@ import { PostsService } from 'app/posts.services'; import { ActivatedRoute, Router, ParamMap } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; import { EditSubscriptionDialogComponent } from 'app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component'; +import type { FileType } from '../../../api-types'; @Component({ selector: 'app-subscription', @@ -151,7 +152,7 @@ export class SubscriptionComponent implements OnInit { } this.downloading = true; - this.postsService.downloadFileFromServer(fileNames, 'video', this.subscription.name, true).subscribe(res => { + this.postsService.downloadFileFromServer(fileNames, 'video' as FileType, this.subscription.name, true).subscribe(res => { this.downloading = false; const blob: Blob = res; saveAs(blob, this.subscription.name + '.zip'); From 70d1afce762f71684823b9f66c558df83d804e4c Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Fri, 25 Sep 2020 10:46:55 -0700 Subject: [PATCH 006/680] Move user schemas so they can be imported --- Public API v1.yaml | 191 +++++++++++++++++++++----------------- src/app/posts.services.ts | 46 +++++---- 2 files changed, 134 insertions(+), 103 deletions(-) diff --git a/Public API v1.yaml b/Public API v1.yaml index e508fd5..70d56c5 100644 --- a/Public API v1.yaml +++ b/Public API v1.yaml @@ -695,35 +695,13 @@ paths: content: application/json: schema: - type: object - properties: - user: - $ref: '#/components/schemas/User' - token: - type: string - permissions: - type: array - items: - type: string - available_permissions: - type: array - items: - type: string + $ref: '#/components/schemas/LoginResponse' description: Use this method to log into a user using their username and password and receive a jwt auth token so you can send per-user requests. requestBody: content: application/json: schema: - type: object - properties: - userid: - type: string - description: 'This is the username, not the user uid' - password: - type: string - required: - - userid - - password + $ref: '#/components/schemas/LoginRequest' security: - Auth query parameter: [] tags: @@ -738,27 +716,13 @@ paths: content: application/json: schema: - type: object - properties: - user: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/RegisterResponse' description: Use this endpoint to register a user. It will only work if registration is enabled. requestBody: content: application/json: schema: - type: object - properties: - userid: - type: string - username: - type: string - password: - type: string - required: - - userid - - username - - password + $ref: '#/components/schemas/RegisterRequest' security: - Auth query parameter: [] tags: @@ -778,21 +742,7 @@ paths: content: application/json: schema: - type: object - properties: - change_object: - required: - - uid - type: object - properties: - uid: - type: string - name: - type: string - role: - type: string - required: - - change_object + $ref: '#/components/schemas/UpdateUserRequest' description: Updates certain properties for a user. Only two are possible right now. security: - Auth query parameter: [] @@ -814,12 +764,7 @@ paths: content: application/json: schema: - type: object - properties: - uid: - type: string - required: - - uid + $ref: '#/components/schemas/DeleteUserRequest' security: - Auth query parameter: [] tags: @@ -834,25 +779,7 @@ paths: content: application/json: schema: - type: object - properties: - roles: - type: object - properties: - admin: - type: object - properties: - permissions: - type: array - items: - $ref: '#/components/schemas/UserPermission' - user: - type: object - properties: - permissions: - type: array - items: - $ref: '#/components/schemas/UserPermission' + $ref: '#/components/schemas/GetRolesResponse' description: Gets the available roles and their permissions security: - Auth query parameter: [] @@ -910,12 +837,7 @@ paths: content: application/json: schema: - type: object - properties: - users: - type: array - items: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/GetUsersResponse' description: 'Gets all users, returns a list of the user objects including their user permissions, videos, playlists, subscriptions, etc.' security: - Auth query parameter: [] @@ -1695,6 +1617,103 @@ components: properties: id: type: string + RegisterRequest: + required: + - userid + - username + - password + type: object + properties: + userid: + type: string + username: + type: string + password: + type: string + RegisterResponse: + type: object + properties: + user: + $ref: '#/components/schemas/User' + LoginRequest: + required: + - username + - password + type: object + properties: + username: + type: string + password: + type: string + LoginResponse: + type: object + properties: + user: + $ref: '#/components/schemas/User' + token: + type: string + permissions: + type: array + items: + $ref: '#/components/schemas/UserPermission' + available_permissions: + type: array + items: + $ref: '#/components/schemas/UserPermission' + UpdateUserRequest: + required: + - change_object + type: object + properties: + change_object: + required: + - uid + type: object + properties: + uid: + type: string + name: + type: string + role: + type: string + DeleteUserRequest: + required: + - uid + type: object + properties: + uid: + type: string + GetUsersResponse: + required: + - users + type: object + properties: + users: + type: array + items: + $ref: '#/components/schemas/User' + GetRolesResponse: + required: + - roles + type: object + properties: + roles: + type: object + properties: + admin: + type: object + properties: + permissions: + type: array + items: + $ref: '#/components/schemas/UserPermission' + user: + type: object + properties: + permissions: + type: array + items: + $ref: '#/components/schemas/UserPermission' securitySchemes: Auth query parameter: name: apiKey diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index d0fad49..976298c 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -19,6 +19,7 @@ import type { DeleteMp3Mp4Request, DeletePlaylistRequest, DeleteSubscriptionFileRequest, + DeleteUserRequest, DownloadArchiveRequest, DownloadFileRequest, FileType, @@ -34,13 +35,19 @@ import type { GetMp4sResponse, GetPlaylistRequest, GetPlaylistResponse, + GetRolesResponse, GetSubscriptionRequest, GetSubscriptionResponse, + GetUsersResponse, + LoginRequest, + LoginResponse, Mp3DownloadRequest, Mp3DownloadResponse, Mp4DownloadRequest, Mp4DownloadResponse, Playlist, + RegisterRequest, + RegisterResponse, SetConfigRequest, SharingToggle, SubscribeRequest, @@ -53,6 +60,7 @@ import type { UpdatePlaylistFilesRequest, UpdatePlaylistRequest, UpdateServerRequest, + UpdateUserRequest, UserPermission, YesNo, } from '../api-types'; @@ -444,9 +452,9 @@ export class PostsService implements CanActivate { } // user methods - login(username, password) { - const call = this.http.post(this.path + 'auth/login', {username: username, password: password}, this.httpOptions); - return call; + login(username: string, password: string) { + const body: LoginRequest = {username: username, password: password}; + return this.http.post(this.path + 'auth/login', body, this.httpOptions); } // user methods @@ -481,10 +489,11 @@ export class PostsService implements CanActivate { } // user methods - register(username, password) { - const call = this.http.post(this.path + 'auth/register', {userid: username, - username: username, - password: password}, this.httpOptions); + register(username: string, password: string) { + const body: RegisterRequest = {userid: username, + username: username, + password: password} + const call = this.http.post(this.path + 'auth/register', body, this.httpOptions); return call; } @@ -529,10 +538,11 @@ export class PostsService implements CanActivate { return this.http.post(this.path + 'auth/adminExists', {}, this.httpOptions); } - createAdminAccount(password) { - return this.http.post(this.path + 'auth/register', {userid: 'admin', - username: 'admin', - password: password}, this.httpOptions); + createAdminAccount(password: string) { + const body: RegisterRequest = {userid: 'admin', + username: 'admin', + password: password}; + return this.http.post(this.path + 'auth/register', body, this.httpOptions); } checkAdminCreationStatus(force_show = false) { @@ -547,12 +557,14 @@ export class PostsService implements CanActivate { }); } - changeUser(change_obj) { - return this.http.post(this.path + 'updateUser', {change_object: change_obj}, this.httpOptions); + changeUser(change_obj: UpdateUserRequest['change_object']) { + const body: UpdateUserRequest = {change_object: change_obj}; + return this.http.post(this.path + 'updateUser', body, this.httpOptions); } - deleteUser(uid) { - return this.http.post(this.path + 'deleteUser', {uid: uid}, this.httpOptions); + deleteUser(uid: string) { + const body: DeleteUserRequest = {uid: uid}; + return this.http.post(this.path + 'deleteUser', body, this.httpOptions); } changeUserPassword(user_uid, new_password) { @@ -560,11 +572,11 @@ export class PostsService implements CanActivate { } getUsers() { - return this.http.post(this.path + 'getUsers', {}, this.httpOptions); + return this.http.post(this.path + 'getUsers', {}, this.httpOptions); } getRoles() { - return this.http.post(this.path + 'getRoles', {}, this.httpOptions); + return this.http.post(this.path + 'getRoles', {}, this.httpOptions); } setUserPermission(user_uid: string, permission: UserPermission, new_value: YesNo) { From 111254824631891d53d91b9ad05a5f176c3376d9 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Fri, 25 Sep 2020 10:47:25 -0700 Subject: [PATCH 007/680] Commit api types --- .gitignore | 1 - src/api-types/index.ts | 70 +++++++++++++++++++ .../models/BaseChangePermissionsRequest.ts | 11 +++ src/api-types/models/BaseDownloadRequest.ts | 29 ++++++++ src/api-types/models/BaseDownloadResponse.ts | 9 +++ .../models/ChangeRolePermissionsRequest.ts | 9 +++ .../models/ChangeUserPermissionsRequest.ts | 9 +++ src/api-types/models/Config.ts | 8 +++ src/api-types/models/ConfigResponse.ts | 10 +++ src/api-types/models/CreatePlaylistRequest.ts | 13 ++++ .../models/CreatePlaylistResponse.ts | 10 +++ src/api-types/models/DatabaseFile.ts | 22 ++++++ src/api-types/models/DeleteFileRequest.ts | 10 +++ src/api-types/models/DeleteMp3Mp4Request.ts | 9 +++ src/api-types/models/DeletePlaylistRequest.ts | 10 +++ .../models/DeleteSubscriptionFileRequest.ts | 15 ++++ src/api-types/models/DeleteUserRequest.ts | 8 +++ src/api-types/models/Dictionary.ts | 7 ++ src/api-types/models/Download.ts | 23 ++++++ .../models/DownloadArchiveRequest.ts | 10 +++ src/api-types/models/DownloadFileRequest.ts | 22 ++++++ .../DownloadVideosForSubscriptionRequest.ts | 8 +++ src/api-types/models/File.ts | 8 +++ src/api-types/models/FileType.ts | 9 +++ .../models/GenerateNewApiKeyResponse.ts | 8 +++ .../models/GetAllDownloadsResponse.ts | 13 ++++ src/api-types/models/GetAllFilesResponse.ts | 14 ++++ .../models/GetAllSubscriptionsResponse.ts | 9 +++ src/api-types/models/GetDownloadRequest.ts | 9 +++ src/api-types/models/GetDownloadResponse.ts | 9 +++ src/api-types/models/GetFileRequest.ts | 17 +++++ src/api-types/models/GetFileResponse.ts | 10 +++ src/api-types/models/GetMp3sResponse.ts | 14 ++++ src/api-types/models/GetMp4sResponse.ts | 14 ++++ src/api-types/models/GetPlaylistRequest.ts | 11 +++ src/api-types/models/GetPlaylistResponse.ts | 12 ++++ src/api-types/models/GetRolesResponse.ts | 16 +++++ .../models/GetSubscriptionRequest.ts | 11 +++ .../models/GetSubscriptionResponse.ts | 10 +++ src/api-types/models/GetUsersResponse.ts | 9 +++ src/api-types/models/LoginRequest.ts | 9 +++ src/api-types/models/LoginResponse.ts | 13 ++++ src/api-types/models/Mp3DownloadRequest.ts | 12 ++++ src/api-types/models/Mp3DownloadResponse.ts | 9 +++ src/api-types/models/Mp4DownloadRequest.ts | 12 ++++ src/api-types/models/Mp4DownloadResponse.ts | 9 +++ src/api-types/models/Playlist.ts | 15 ++++ src/api-types/models/RegisterRequest.ts | 10 +++ src/api-types/models/RegisterResponse.ts | 9 +++ src/api-types/models/SetConfigRequest.ts | 9 +++ src/api-types/models/SharingToggle.ts | 11 +++ src/api-types/models/SubscribeRequest.ts | 14 ++++ src/api-types/models/SubscribeResponse.ts | 10 +++ src/api-types/models/Subscription.ts | 20 ++++++ .../models/SubscriptionRequestData.ts | 13 ++++ src/api-types/models/SuccessObject.ts | 8 +++ src/api-types/models/UnsubscribeRequest.ts | 13 ++++ src/api-types/models/UnsubscribeResponse.ts | 9 +++ .../models/UpdatePlaylistFilesRequest.ts | 11 +++ src/api-types/models/UpdatePlaylistRequest.ts | 9 +++ src/api-types/models/UpdateServerRequest.ts | 8 +++ src/api-types/models/UpdateUserRequest.ts | 12 ++++ src/api-types/models/UpdaterStatus.ts | 10 +++ src/api-types/models/User.ts | 25 +++++++ src/api-types/models/UserPermission.ts | 13 ++++ src/api-types/models/YesNo.ts | 9 +++ src/api-types/models/body_19.ts | 8 +++ src/api-types/models/body_20.ts | 8 +++ .../models/inline_response_200_15.ts | 8 +++ 69 files changed, 851 insertions(+), 1 deletion(-) create mode 100644 src/api-types/index.ts create mode 100644 src/api-types/models/BaseChangePermissionsRequest.ts create mode 100644 src/api-types/models/BaseDownloadRequest.ts create mode 100644 src/api-types/models/BaseDownloadResponse.ts create mode 100644 src/api-types/models/ChangeRolePermissionsRequest.ts create mode 100644 src/api-types/models/ChangeUserPermissionsRequest.ts create mode 100644 src/api-types/models/Config.ts create mode 100644 src/api-types/models/ConfigResponse.ts create mode 100644 src/api-types/models/CreatePlaylistRequest.ts create mode 100644 src/api-types/models/CreatePlaylistResponse.ts create mode 100644 src/api-types/models/DatabaseFile.ts create mode 100644 src/api-types/models/DeleteFileRequest.ts create mode 100644 src/api-types/models/DeleteMp3Mp4Request.ts create mode 100644 src/api-types/models/DeletePlaylistRequest.ts create mode 100644 src/api-types/models/DeleteSubscriptionFileRequest.ts create mode 100644 src/api-types/models/DeleteUserRequest.ts create mode 100644 src/api-types/models/Dictionary.ts create mode 100644 src/api-types/models/Download.ts create mode 100644 src/api-types/models/DownloadArchiveRequest.ts create mode 100644 src/api-types/models/DownloadFileRequest.ts create mode 100644 src/api-types/models/DownloadVideosForSubscriptionRequest.ts create mode 100644 src/api-types/models/File.ts create mode 100644 src/api-types/models/FileType.ts create mode 100644 src/api-types/models/GenerateNewApiKeyResponse.ts create mode 100644 src/api-types/models/GetAllDownloadsResponse.ts create mode 100644 src/api-types/models/GetAllFilesResponse.ts create mode 100644 src/api-types/models/GetAllSubscriptionsResponse.ts create mode 100644 src/api-types/models/GetDownloadRequest.ts create mode 100644 src/api-types/models/GetDownloadResponse.ts create mode 100644 src/api-types/models/GetFileRequest.ts create mode 100644 src/api-types/models/GetFileResponse.ts create mode 100644 src/api-types/models/GetMp3sResponse.ts create mode 100644 src/api-types/models/GetMp4sResponse.ts create mode 100644 src/api-types/models/GetPlaylistRequest.ts create mode 100644 src/api-types/models/GetPlaylistResponse.ts create mode 100644 src/api-types/models/GetRolesResponse.ts create mode 100644 src/api-types/models/GetSubscriptionRequest.ts create mode 100644 src/api-types/models/GetSubscriptionResponse.ts create mode 100644 src/api-types/models/GetUsersResponse.ts create mode 100644 src/api-types/models/LoginRequest.ts create mode 100644 src/api-types/models/LoginResponse.ts create mode 100644 src/api-types/models/Mp3DownloadRequest.ts create mode 100644 src/api-types/models/Mp3DownloadResponse.ts create mode 100644 src/api-types/models/Mp4DownloadRequest.ts create mode 100644 src/api-types/models/Mp4DownloadResponse.ts create mode 100644 src/api-types/models/Playlist.ts create mode 100644 src/api-types/models/RegisterRequest.ts create mode 100644 src/api-types/models/RegisterResponse.ts create mode 100644 src/api-types/models/SetConfigRequest.ts create mode 100644 src/api-types/models/SharingToggle.ts create mode 100644 src/api-types/models/SubscribeRequest.ts create mode 100644 src/api-types/models/SubscribeResponse.ts create mode 100644 src/api-types/models/Subscription.ts create mode 100644 src/api-types/models/SubscriptionRequestData.ts create mode 100644 src/api-types/models/SuccessObject.ts create mode 100644 src/api-types/models/UnsubscribeRequest.ts create mode 100644 src/api-types/models/UnsubscribeResponse.ts create mode 100644 src/api-types/models/UpdatePlaylistFilesRequest.ts create mode 100644 src/api-types/models/UpdatePlaylistRequest.ts create mode 100644 src/api-types/models/UpdateServerRequest.ts create mode 100644 src/api-types/models/UpdateUserRequest.ts create mode 100644 src/api-types/models/UpdaterStatus.ts create mode 100644 src/api-types/models/User.ts create mode 100644 src/api-types/models/UserPermission.ts create mode 100644 src/api-types/models/YesNo.ts create mode 100644 src/api-types/models/body_19.ts create mode 100644 src/api-types/models/body_20.ts create mode 100644 src/api-types/models/inline_response_200_15.ts diff --git a/.gitignore b/.gitignore index 32c9fd9..c99f53f 100644 --- a/.gitignore +++ b/.gitignore @@ -55,7 +55,6 @@ backend/subscriptions/playlists/* backend/subscriptions/archives/* backend/*.exe src/assets/default.json -src/api-types backend/appdata/db.json backend/appdata/archives/archive_audio.txt backend/appdata/archives/archive_video.txt diff --git a/src/api-types/index.ts b/src/api-types/index.ts new file mode 100644 index 0000000..5a0b927 --- /dev/null +++ b/src/api-types/index.ts @@ -0,0 +1,70 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type { BaseChangePermissionsRequest } from './models/BaseChangePermissionsRequest'; +export type { BaseDownloadRequest } from './models/BaseDownloadRequest'; +export type { BaseDownloadResponse } from './models/BaseDownloadResponse'; +export type { body_19 } from './models/body_19'; +export type { body_20 } from './models/body_20'; +export type { ChangeRolePermissionsRequest } from './models/ChangeRolePermissionsRequest'; +export type { ChangeUserPermissionsRequest } from './models/ChangeUserPermissionsRequest'; +export type { Config } from './models/Config'; +export type { ConfigResponse } from './models/ConfigResponse'; +export type { CreatePlaylistRequest } from './models/CreatePlaylistRequest'; +export type { CreatePlaylistResponse } from './models/CreatePlaylistResponse'; +export type { DatabaseFile } from './models/DatabaseFile'; +export type { DeleteFileRequest } from './models/DeleteFileRequest'; +export type { DeleteMp3Mp4Request } from './models/DeleteMp3Mp4Request'; +export type { DeletePlaylistRequest } from './models/DeletePlaylistRequest'; +export type { DeleteSubscriptionFileRequest } from './models/DeleteSubscriptionFileRequest'; +export type { DeleteUserRequest } from './models/DeleteUserRequest'; +export type { Download } from './models/Download'; +export type { DownloadArchiveRequest } from './models/DownloadArchiveRequest'; +export type { DownloadFileRequest } from './models/DownloadFileRequest'; +export type { DownloadVideosForSubscriptionRequest } from './models/DownloadVideosForSubscriptionRequest'; +export type { File } from './models/File'; +export { FileType } from './models/FileType'; +export type { GenerateNewApiKeyResponse } from './models/GenerateNewApiKeyResponse'; +export type { GetAllDownloadsResponse } from './models/GetAllDownloadsResponse'; +export type { GetAllFilesResponse } from './models/GetAllFilesResponse'; +export type { GetAllSubscriptionsResponse } from './models/GetAllSubscriptionsResponse'; +export type { GetDownloadRequest } from './models/GetDownloadRequest'; +export type { GetDownloadResponse } from './models/GetDownloadResponse'; +export type { GetFileRequest } from './models/GetFileRequest'; +export type { GetFileResponse } from './models/GetFileResponse'; +export type { GetMp3sResponse } from './models/GetMp3sResponse'; +export type { GetMp4sResponse } from './models/GetMp4sResponse'; +export type { GetPlaylistRequest } from './models/GetPlaylistRequest'; +export type { GetPlaylistResponse } from './models/GetPlaylistResponse'; +export type { GetRolesResponse } from './models/GetRolesResponse'; +export type { GetSubscriptionRequest } from './models/GetSubscriptionRequest'; +export type { GetSubscriptionResponse } from './models/GetSubscriptionResponse'; +export type { GetUsersResponse } from './models/GetUsersResponse'; +export type { inline_response_200_15 } from './models/inline_response_200_15'; +export type { LoginRequest } from './models/LoginRequest'; +export type { LoginResponse } from './models/LoginResponse'; +export type { Mp3DownloadRequest } from './models/Mp3DownloadRequest'; +export type { Mp3DownloadResponse } from './models/Mp3DownloadResponse'; +export type { Mp4DownloadRequest } from './models/Mp4DownloadRequest'; +export type { Mp4DownloadResponse } from './models/Mp4DownloadResponse'; +export type { Playlist } from './models/Playlist'; +export type { RegisterRequest } from './models/RegisterRequest'; +export type { RegisterResponse } from './models/RegisterResponse'; +export type { SetConfigRequest } from './models/SetConfigRequest'; +export type { SharingToggle } from './models/SharingToggle'; +export type { SubscribeRequest } from './models/SubscribeRequest'; +export type { SubscribeResponse } from './models/SubscribeResponse'; +export type { Subscription } from './models/Subscription'; +export type { SubscriptionRequestData } from './models/SubscriptionRequestData'; +export type { SuccessObject } from './models/SuccessObject'; +export type { UnsubscribeRequest } from './models/UnsubscribeRequest'; +export type { UnsubscribeResponse } from './models/UnsubscribeResponse'; +export type { UpdatePlaylistFilesRequest } from './models/UpdatePlaylistFilesRequest'; +export type { UpdatePlaylistRequest } from './models/UpdatePlaylistRequest'; +export type { UpdaterStatus } from './models/UpdaterStatus'; +export type { UpdateServerRequest } from './models/UpdateServerRequest'; +export type { UpdateUserRequest } from './models/UpdateUserRequest'; +export type { User } from './models/User'; +export { UserPermission } from './models/UserPermission'; +export { YesNo } from './models/YesNo'; diff --git a/src/api-types/models/BaseChangePermissionsRequest.ts b/src/api-types/models/BaseChangePermissionsRequest.ts new file mode 100644 index 0000000..6551d46 --- /dev/null +++ b/src/api-types/models/BaseChangePermissionsRequest.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { UserPermission } from './UserPermission'; +import { YesNo } from './YesNo'; + +export interface BaseChangePermissionsRequest { + permission: UserPermission; + new_value: YesNo; +} diff --git a/src/api-types/models/BaseDownloadRequest.ts b/src/api-types/models/BaseDownloadRequest.ts new file mode 100644 index 0000000..b6cf3fb --- /dev/null +++ b/src/api-types/models/BaseDownloadRequest.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface BaseDownloadRequest { + url: string; + /** + * Video format code. Overrides other quality options. + */ + customQualityConfiguration?: string; + /** + * Custom command-line arguments for youtubedl. Overrides all other options, except url. + */ + customArgs?: string; + /** + * Custom output filename template. + */ + customOutput?: string; + /** + * Login with this account ID + */ + youtubeUsername?: string; + /** + * Account password + */ + youtubePassword?: string; + ui_uid?: string | null; +} diff --git a/src/api-types/models/BaseDownloadResponse.ts b/src/api-types/models/BaseDownloadResponse.ts new file mode 100644 index 0000000..83d848f --- /dev/null +++ b/src/api-types/models/BaseDownloadResponse.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface BaseDownloadResponse { + uid: string; + file_names?: Array | null; +} diff --git a/src/api-types/models/ChangeRolePermissionsRequest.ts b/src/api-types/models/ChangeRolePermissionsRequest.ts new file mode 100644 index 0000000..a6f6ed9 --- /dev/null +++ b/src/api-types/models/ChangeRolePermissionsRequest.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { BaseChangePermissionsRequest } from './BaseChangePermissionsRequest'; + +export interface ChangeRolePermissionsRequest extends BaseChangePermissionsRequest { + role: string; +} diff --git a/src/api-types/models/ChangeUserPermissionsRequest.ts b/src/api-types/models/ChangeUserPermissionsRequest.ts new file mode 100644 index 0000000..e6f044a --- /dev/null +++ b/src/api-types/models/ChangeUserPermissionsRequest.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { BaseChangePermissionsRequest } from './BaseChangePermissionsRequest'; + +export interface ChangeUserPermissionsRequest extends BaseChangePermissionsRequest { + user_uid: string; +} diff --git a/src/api-types/models/Config.ts b/src/api-types/models/Config.ts new file mode 100644 index 0000000..509c42f --- /dev/null +++ b/src/api-types/models/Config.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface Config { + YoutubeDLMaterial: any; +} diff --git a/src/api-types/models/ConfigResponse.ts b/src/api-types/models/ConfigResponse.ts new file mode 100644 index 0000000..b493361 --- /dev/null +++ b/src/api-types/models/ConfigResponse.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { Config } from './Config'; + +export interface ConfigResponse { + config_file: Config; + success: boolean; +} diff --git a/src/api-types/models/CreatePlaylistRequest.ts b/src/api-types/models/CreatePlaylistRequest.ts new file mode 100644 index 0000000..e497dff --- /dev/null +++ b/src/api-types/models/CreatePlaylistRequest.ts @@ -0,0 +1,13 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { FileType } from './FileType'; + +export interface CreatePlaylistRequest { + playlistName: string; + fileNames: Array; + type: FileType; + thumbnailURL: string; + duration: number; +} diff --git a/src/api-types/models/CreatePlaylistResponse.ts b/src/api-types/models/CreatePlaylistResponse.ts new file mode 100644 index 0000000..7d5a98c --- /dev/null +++ b/src/api-types/models/CreatePlaylistResponse.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { Playlist } from './Playlist'; + +export interface CreatePlaylistResponse { + new_playlist: Playlist; + success: boolean; +} diff --git a/src/api-types/models/DatabaseFile.ts b/src/api-types/models/DatabaseFile.ts new file mode 100644 index 0000000..d6009b2 --- /dev/null +++ b/src/api-types/models/DatabaseFile.ts @@ -0,0 +1,22 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface DatabaseFile { + id: string; + title: string; + thumbnailURL: string; + isAudio: boolean; + /** + * In seconds + */ + duration: number; + url: string; + uploader: string; + size: number; + path: string; + upload_date: string; + uid: string; + sharingEnabled?: boolean; +} diff --git a/src/api-types/models/DeleteFileRequest.ts b/src/api-types/models/DeleteFileRequest.ts new file mode 100644 index 0000000..c64af6c --- /dev/null +++ b/src/api-types/models/DeleteFileRequest.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { FileType } from './FileType'; + +export interface DeleteFileRequest { + fileName: string; + type: FileType; +} diff --git a/src/api-types/models/DeleteMp3Mp4Request.ts b/src/api-types/models/DeleteMp3Mp4Request.ts new file mode 100644 index 0000000..f6d65d6 --- /dev/null +++ b/src/api-types/models/DeleteMp3Mp4Request.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface DeleteMp3Mp4Request { + uid: string; + blacklistMode?: boolean; +} diff --git a/src/api-types/models/DeletePlaylistRequest.ts b/src/api-types/models/DeletePlaylistRequest.ts new file mode 100644 index 0000000..25830bb --- /dev/null +++ b/src/api-types/models/DeletePlaylistRequest.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { FileType } from './FileType'; + +export interface DeletePlaylistRequest { + playlistID: string; + type: FileType; +} diff --git a/src/api-types/models/DeleteSubscriptionFileRequest.ts b/src/api-types/models/DeleteSubscriptionFileRequest.ts new file mode 100644 index 0000000..aebe91b --- /dev/null +++ b/src/api-types/models/DeleteSubscriptionFileRequest.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { SubscriptionRequestData } from './SubscriptionRequestData'; + +export interface DeleteSubscriptionFileRequest { + file: string; + file_uid?: string; + sub: SubscriptionRequestData; + /** + * If true, does not remove id from archive. Only valid if youtube-dl archive is enabled in settings. + */ + deleteForever?: boolean; +} diff --git a/src/api-types/models/DeleteUserRequest.ts b/src/api-types/models/DeleteUserRequest.ts new file mode 100644 index 0000000..a994369 --- /dev/null +++ b/src/api-types/models/DeleteUserRequest.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface DeleteUserRequest { + uid: string; +} diff --git a/src/api-types/models/Dictionary.ts b/src/api-types/models/Dictionary.ts new file mode 100644 index 0000000..8ba0443 --- /dev/null +++ b/src/api-types/models/Dictionary.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type Dictionary = { + [key: string]: T; +} diff --git a/src/api-types/models/Download.ts b/src/api-types/models/Download.ts new file mode 100644 index 0000000..b4d91d9 --- /dev/null +++ b/src/api-types/models/Download.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface Download { + uid: string; + ui_uid: string; + downloading: boolean; + complete: boolean; + url: string; + type: string; + percent_complete: number; + is_playlist: boolean; + timestamp_start: number; + timestamp_end?: number; + filesize?: number | null; + /** + * Error text, set if download fails. + */ + error?: string; + fileNames?: Array; +} diff --git a/src/api-types/models/DownloadArchiveRequest.ts b/src/api-types/models/DownloadArchiveRequest.ts new file mode 100644 index 0000000..46c5c45 --- /dev/null +++ b/src/api-types/models/DownloadArchiveRequest.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface DownloadArchiveRequest { + sub: { + archive_dir: string, + }; +} diff --git a/src/api-types/models/DownloadFileRequest.ts b/src/api-types/models/DownloadFileRequest.ts new file mode 100644 index 0000000..18c6cac --- /dev/null +++ b/src/api-types/models/DownloadFileRequest.ts @@ -0,0 +1,22 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { FileType } from './FileType'; + +export interface DownloadFileRequest { + fileNames: ; + zip_mode?: boolean; + type: FileType; + outputName?: string; + fullPathProvided?: boolean; + uuid?: string; + /** + * Only used for subscriptions + */ + subscriptionName?: boolean; + /** + * Only used for subscriptions + */ + subPlaylist?: boolean; +} diff --git a/src/api-types/models/DownloadVideosForSubscriptionRequest.ts b/src/api-types/models/DownloadVideosForSubscriptionRequest.ts new file mode 100644 index 0000000..4b64af3 --- /dev/null +++ b/src/api-types/models/DownloadVideosForSubscriptionRequest.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface DownloadVideosForSubscriptionRequest { + subID: string; +} diff --git a/src/api-types/models/File.ts b/src/api-types/models/File.ts new file mode 100644 index 0000000..2e81613 --- /dev/null +++ b/src/api-types/models/File.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface File { + id?: string; +} diff --git a/src/api-types/models/FileType.ts b/src/api-types/models/FileType.ts new file mode 100644 index 0000000..e1b6f79 --- /dev/null +++ b/src/api-types/models/FileType.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export enum FileType { + AUDIO = 'audio', + VIDEO = 'video', +} \ No newline at end of file diff --git a/src/api-types/models/GenerateNewApiKeyResponse.ts b/src/api-types/models/GenerateNewApiKeyResponse.ts new file mode 100644 index 0000000..d96a464 --- /dev/null +++ b/src/api-types/models/GenerateNewApiKeyResponse.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface GenerateNewApiKeyResponse { + new_api_key: string; +} diff --git a/src/api-types/models/GetAllDownloadsResponse.ts b/src/api-types/models/GetAllDownloadsResponse.ts new file mode 100644 index 0000000..1f99a85 --- /dev/null +++ b/src/api-types/models/GetAllDownloadsResponse.ts @@ -0,0 +1,13 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { Dictionary } from './Dictionary'; +import { Download } from './Download'; + +export interface GetAllDownloadsResponse { + /** + * Map of Session ID to inner map + */ + downloads?: Dictionary>; +} diff --git a/src/api-types/models/GetAllFilesResponse.ts b/src/api-types/models/GetAllFilesResponse.ts new file mode 100644 index 0000000..79f7fc6 --- /dev/null +++ b/src/api-types/models/GetAllFilesResponse.ts @@ -0,0 +1,14 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { DatabaseFile } from './DatabaseFile'; +import { Playlist } from './Playlist'; + +export interface GetAllFilesResponse { + files: Array; + /** + * All video playlists + */ + playlists: Array; +} diff --git a/src/api-types/models/GetAllSubscriptionsResponse.ts b/src/api-types/models/GetAllSubscriptionsResponse.ts new file mode 100644 index 0000000..fafb6f5 --- /dev/null +++ b/src/api-types/models/GetAllSubscriptionsResponse.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { Subscription } from './Subscription'; + +export interface GetAllSubscriptionsResponse { + subscriptions: Array; +} diff --git a/src/api-types/models/GetDownloadRequest.ts b/src/api-types/models/GetDownloadRequest.ts new file mode 100644 index 0000000..9ad5fdf --- /dev/null +++ b/src/api-types/models/GetDownloadRequest.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface GetDownloadRequest { + session_id: string; + download_id: string; +} diff --git a/src/api-types/models/GetDownloadResponse.ts b/src/api-types/models/GetDownloadResponse.ts new file mode 100644 index 0000000..b027cbf --- /dev/null +++ b/src/api-types/models/GetDownloadResponse.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { Download } from './Download'; + +export interface GetDownloadResponse { + download?: Download | null; +} diff --git a/src/api-types/models/GetFileRequest.ts b/src/api-types/models/GetFileRequest.ts new file mode 100644 index 0000000..0d85afb --- /dev/null +++ b/src/api-types/models/GetFileRequest.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { FileType } from './FileType'; + +export interface GetFileRequest { + /** + * Video UID + */ + uid: string; + type?: FileType; + /** + * User UID + */ + uuid?: string; +} diff --git a/src/api-types/models/GetFileResponse.ts b/src/api-types/models/GetFileResponse.ts new file mode 100644 index 0000000..71b3563 --- /dev/null +++ b/src/api-types/models/GetFileResponse.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { DatabaseFile } from './DatabaseFile'; + +export interface GetFileResponse { + success: boolean; + file?: DatabaseFile; +} diff --git a/src/api-types/models/GetMp3sResponse.ts b/src/api-types/models/GetMp3sResponse.ts new file mode 100644 index 0000000..13f03dc --- /dev/null +++ b/src/api-types/models/GetMp3sResponse.ts @@ -0,0 +1,14 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { DatabaseFile } from './DatabaseFile'; +import { Playlist } from './Playlist'; + +export interface GetMp3sResponse { + mp3s: Array; + /** + * All audio playlists + */ + playlists: Array; +} diff --git a/src/api-types/models/GetMp4sResponse.ts b/src/api-types/models/GetMp4sResponse.ts new file mode 100644 index 0000000..9ef2c1f --- /dev/null +++ b/src/api-types/models/GetMp4sResponse.ts @@ -0,0 +1,14 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { DatabaseFile } from './DatabaseFile'; +import { Playlist } from './Playlist'; + +export interface GetMp4sResponse { + mp4s: Array; + /** + * All video playlists + */ + playlists: Array; +} diff --git a/src/api-types/models/GetPlaylistRequest.ts b/src/api-types/models/GetPlaylistRequest.ts new file mode 100644 index 0000000..ca86a2d --- /dev/null +++ b/src/api-types/models/GetPlaylistRequest.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { FileType } from './FileType'; + +export interface GetPlaylistRequest { + playlistID: string; + type?: FileType; + uuid?: string; +} diff --git a/src/api-types/models/GetPlaylistResponse.ts b/src/api-types/models/GetPlaylistResponse.ts new file mode 100644 index 0000000..c3bf7c9 --- /dev/null +++ b/src/api-types/models/GetPlaylistResponse.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { FileType } from './FileType'; +import { Playlist } from './Playlist'; + +export interface GetPlaylistResponse { + playlist: Playlist; + type: FileType; + success: boolean; +} diff --git a/src/api-types/models/GetRolesResponse.ts b/src/api-types/models/GetRolesResponse.ts new file mode 100644 index 0000000..ce5e696 --- /dev/null +++ b/src/api-types/models/GetRolesResponse.ts @@ -0,0 +1,16 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { UserPermission } from './UserPermission'; + +export interface GetRolesResponse { + roles: { + admin?: { + permissions?: Array, + }, + user?: { + permissions?: Array, + }, + }; +} diff --git a/src/api-types/models/GetSubscriptionRequest.ts b/src/api-types/models/GetSubscriptionRequest.ts new file mode 100644 index 0000000..99ee221 --- /dev/null +++ b/src/api-types/models/GetSubscriptionRequest.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface GetSubscriptionRequest { + /** + * Subscription ID + */ + id: string; +} diff --git a/src/api-types/models/GetSubscriptionResponse.ts b/src/api-types/models/GetSubscriptionResponse.ts new file mode 100644 index 0000000..9d2209a --- /dev/null +++ b/src/api-types/models/GetSubscriptionResponse.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { Subscription } from './Subscription'; + +export interface GetSubscriptionResponse { + subscription: Subscription; + files: Array; +} diff --git a/src/api-types/models/GetUsersResponse.ts b/src/api-types/models/GetUsersResponse.ts new file mode 100644 index 0000000..f5f9eb0 --- /dev/null +++ b/src/api-types/models/GetUsersResponse.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { User } from './User'; + +export interface GetUsersResponse { + users: Array; +} diff --git a/src/api-types/models/LoginRequest.ts b/src/api-types/models/LoginRequest.ts new file mode 100644 index 0000000..a06327f --- /dev/null +++ b/src/api-types/models/LoginRequest.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface LoginRequest { + username: string; + password: string; +} diff --git a/src/api-types/models/LoginResponse.ts b/src/api-types/models/LoginResponse.ts new file mode 100644 index 0000000..c94f444 --- /dev/null +++ b/src/api-types/models/LoginResponse.ts @@ -0,0 +1,13 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { User } from './User'; +import { UserPermission } from './UserPermission'; + +export interface LoginResponse { + user?: User; + token?: string; + permissions?: Array; + available_permissions?: Array; +} diff --git a/src/api-types/models/Mp3DownloadRequest.ts b/src/api-types/models/Mp3DownloadRequest.ts new file mode 100644 index 0000000..b4b4f8f --- /dev/null +++ b/src/api-types/models/Mp3DownloadRequest.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { BaseDownloadRequest } from './BaseDownloadRequest'; + +export interface Mp3DownloadRequest extends BaseDownloadRequest { + /** + * Specify ffmpeg/avconv audio quality + */ + maxBitrate?: string; +} diff --git a/src/api-types/models/Mp3DownloadResponse.ts b/src/api-types/models/Mp3DownloadResponse.ts new file mode 100644 index 0000000..32924a6 --- /dev/null +++ b/src/api-types/models/Mp3DownloadResponse.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { BaseDownloadResponse } from './BaseDownloadResponse'; + +export interface Mp3DownloadResponse extends BaseDownloadResponse { + audiopathEncoded: string; +} diff --git a/src/api-types/models/Mp4DownloadRequest.ts b/src/api-types/models/Mp4DownloadRequest.ts new file mode 100644 index 0000000..faeb6ab --- /dev/null +++ b/src/api-types/models/Mp4DownloadRequest.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { BaseDownloadRequest } from './BaseDownloadRequest'; + +export interface Mp4DownloadRequest extends BaseDownloadRequest { + /** + * Height of the video, if known + */ + selectedHeight?: string; +} diff --git a/src/api-types/models/Mp4DownloadResponse.ts b/src/api-types/models/Mp4DownloadResponse.ts new file mode 100644 index 0000000..0763c07 --- /dev/null +++ b/src/api-types/models/Mp4DownloadResponse.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { BaseDownloadResponse } from './BaseDownloadResponse'; + +export interface Mp4DownloadResponse extends BaseDownloadResponse { + videopathEncoded: string; +} diff --git a/src/api-types/models/Playlist.ts b/src/api-types/models/Playlist.ts new file mode 100644 index 0000000..1afe993 --- /dev/null +++ b/src/api-types/models/Playlist.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { FileType } from './FileType'; + +export interface Playlist { + name: string; + fileNames: Array; + id: string; + thumbnailURL: string; + type: FileType; + registered: number; + duration: number; +} diff --git a/src/api-types/models/RegisterRequest.ts b/src/api-types/models/RegisterRequest.ts new file mode 100644 index 0000000..73bee22 --- /dev/null +++ b/src/api-types/models/RegisterRequest.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface RegisterRequest { + userid: string; + username: string; + password: string; +} diff --git a/src/api-types/models/RegisterResponse.ts b/src/api-types/models/RegisterResponse.ts new file mode 100644 index 0000000..4e45b65 --- /dev/null +++ b/src/api-types/models/RegisterResponse.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { User } from './User'; + +export interface RegisterResponse { + user?: User; +} diff --git a/src/api-types/models/SetConfigRequest.ts b/src/api-types/models/SetConfigRequest.ts new file mode 100644 index 0000000..41ef52a --- /dev/null +++ b/src/api-types/models/SetConfigRequest.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { Config } from './Config'; + +export interface SetConfigRequest { + new_config_file: Config; +} diff --git a/src/api-types/models/SharingToggle.ts b/src/api-types/models/SharingToggle.ts new file mode 100644 index 0000000..2de90fc --- /dev/null +++ b/src/api-types/models/SharingToggle.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { FileType } from './FileType'; + +export interface SharingToggle { + uid: string; + type: FileType; + is_playlist?: boolean; +} diff --git a/src/api-types/models/SubscribeRequest.ts b/src/api-types/models/SubscribeRequest.ts new file mode 100644 index 0000000..d475cb9 --- /dev/null +++ b/src/api-types/models/SubscribeRequest.ts @@ -0,0 +1,14 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface SubscribeRequest { + name: string; + url: string; + timerange?: string; + streamingOnly: boolean; + audioOnly?: boolean; + customArgs?: string; + customFileOutput?: string; +} diff --git a/src/api-types/models/SubscribeResponse.ts b/src/api-types/models/SubscribeResponse.ts new file mode 100644 index 0000000..fa4c235 --- /dev/null +++ b/src/api-types/models/SubscribeResponse.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { Subscription } from './Subscription'; + +export interface SubscribeResponse { + new_sub: Subscription; + error?: string; +} diff --git a/src/api-types/models/Subscription.ts b/src/api-types/models/Subscription.ts new file mode 100644 index 0000000..8cd369e --- /dev/null +++ b/src/api-types/models/Subscription.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { FileType } from './FileType'; + +export interface Subscription { + name: string; + url: string; + id: string; + type: FileType; + user_uid: string | null; + streamingOnly: boolean; + isPlaylist: boolean; + archive?: string; + timerange?: string; + custom_args?: string; + custom_output?: string; + videos: Array; +} diff --git a/src/api-types/models/SubscriptionRequestData.ts b/src/api-types/models/SubscriptionRequestData.ts new file mode 100644 index 0000000..6c99499 --- /dev/null +++ b/src/api-types/models/SubscriptionRequestData.ts @@ -0,0 +1,13 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { FileType } from './FileType'; + +export interface SubscriptionRequestData { + name: string; + id: string; + type?: FileType; + isPlaylist?: boolean; + archive?: string; +} diff --git a/src/api-types/models/SuccessObject.ts b/src/api-types/models/SuccessObject.ts new file mode 100644 index 0000000..cd0a11e --- /dev/null +++ b/src/api-types/models/SuccessObject.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface SuccessObject { + success: boolean; +} diff --git a/src/api-types/models/UnsubscribeRequest.ts b/src/api-types/models/UnsubscribeRequest.ts new file mode 100644 index 0000000..598faee --- /dev/null +++ b/src/api-types/models/UnsubscribeRequest.ts @@ -0,0 +1,13 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { SubscriptionRequestData } from './SubscriptionRequestData'; + +export interface UnsubscribeRequest { + sub: SubscriptionRequestData; + /** + * Defaults to false + */ + deleteMode?: boolean; +} diff --git a/src/api-types/models/UnsubscribeResponse.ts b/src/api-types/models/UnsubscribeResponse.ts new file mode 100644 index 0000000..0f0d991 --- /dev/null +++ b/src/api-types/models/UnsubscribeResponse.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface UnsubscribeResponse { + success: boolean; + error?: string; +} diff --git a/src/api-types/models/UpdatePlaylistFilesRequest.ts b/src/api-types/models/UpdatePlaylistFilesRequest.ts new file mode 100644 index 0000000..3fa95c2 --- /dev/null +++ b/src/api-types/models/UpdatePlaylistFilesRequest.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { FileType } from './FileType'; + +export interface UpdatePlaylistFilesRequest { + playlistID: string; + fileNames: Array; + type: FileType; +} diff --git a/src/api-types/models/UpdatePlaylistRequest.ts b/src/api-types/models/UpdatePlaylistRequest.ts new file mode 100644 index 0000000..1eaee52 --- /dev/null +++ b/src/api-types/models/UpdatePlaylistRequest.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { Playlist } from './Playlist'; + +export interface UpdatePlaylistRequest { + playlist: Playlist; +} diff --git a/src/api-types/models/UpdateServerRequest.ts b/src/api-types/models/UpdateServerRequest.ts new file mode 100644 index 0000000..95db6e4 --- /dev/null +++ b/src/api-types/models/UpdateServerRequest.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface UpdateServerRequest { + tag: string; +} diff --git a/src/api-types/models/UpdateUserRequest.ts b/src/api-types/models/UpdateUserRequest.ts new file mode 100644 index 0000000..c6e2f05 --- /dev/null +++ b/src/api-types/models/UpdateUserRequest.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface UpdateUserRequest { + change_object: { + uid: string, + name?: string, + role?: string, + }; +} diff --git a/src/api-types/models/UpdaterStatus.ts b/src/api-types/models/UpdaterStatus.ts new file mode 100644 index 0000000..19e513c --- /dev/null +++ b/src/api-types/models/UpdaterStatus.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface UpdaterStatus { + updating: boolean; + details: string; + error?: boolean; +} diff --git a/src/api-types/models/User.ts b/src/api-types/models/User.ts new file mode 100644 index 0000000..8a49d3c --- /dev/null +++ b/src/api-types/models/User.ts @@ -0,0 +1,25 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { Subscription } from './Subscription'; +import { UserPermission } from './UserPermission'; + +export interface User { + uid?: string; + name?: string; + passhash?: string; + files?: { + audio?: Array, + video?: Array, + }; + playlists?: { + audio?: Array, + video?: Array, + }; + subscriptions?: Array; + created?: number; + role?: string; + permissions?: Array; + permission_overrides?: Array; +} diff --git a/src/api-types/models/UserPermission.ts b/src/api-types/models/UserPermission.ts new file mode 100644 index 0000000..b22ef87 --- /dev/null +++ b/src/api-types/models/UserPermission.ts @@ -0,0 +1,13 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export enum UserPermission { + FILEMANAGER = 'filemanager', + SETTINGS = 'settings', + SUBSCRIPTIONS = 'subscriptions', + SHARING = 'sharing', + ADVANCED_DOWNLOAD = 'advanced_download', + DOWNLOADS_MANAGER = 'downloads_manager', +} \ No newline at end of file diff --git a/src/api-types/models/YesNo.ts b/src/api-types/models/YesNo.ts new file mode 100644 index 0000000..dbe9c62 --- /dev/null +++ b/src/api-types/models/YesNo.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export enum YesNo { + YES = 'yes', + NO = 'no', +} \ No newline at end of file diff --git a/src/api-types/models/body_19.ts b/src/api-types/models/body_19.ts new file mode 100644 index 0000000..b0ce9cb --- /dev/null +++ b/src/api-types/models/body_19.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface body_19 { + input_pin: string; +} diff --git a/src/api-types/models/body_20.ts b/src/api-types/models/body_20.ts new file mode 100644 index 0000000..84a7b47 --- /dev/null +++ b/src/api-types/models/body_20.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface body_20 { + unhashed_pin: string; +} diff --git a/src/api-types/models/inline_response_200_15.ts b/src/api-types/models/inline_response_200_15.ts new file mode 100644 index 0000000..bb98ac6 --- /dev/null +++ b/src/api-types/models/inline_response_200_15.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface inline_response_200_15 { + is_set: boolean; +} From d1311d00ea2e06ed900017f91bf52f780a366078 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Sat, 26 Sep 2020 14:50:10 -0700 Subject: [PATCH 008/680] Use named arguments with download file --- Public API v1.yaml | 10 +++++++++- src/api-types/models/DownloadFileRequest.ts | 4 +++- .../custom-playlists.component.ts | 2 +- .../recent-videos/recent-videos.component.ts | 16 +++++++++------- src/app/main/main.component.ts | 2 +- src/app/player/player.component.ts | 12 ++++++++---- src/app/posts.services.ts | 14 ++++++++++---- .../subscription/subscription.component.ts | 2 +- 8 files changed, 42 insertions(+), 20 deletions(-) diff --git a/Public API v1.yaml b/Public API v1.yaml index 70d56c5..866493e 100644 --- a/Public API v1.yaml +++ b/Public API v1.yaml @@ -127,6 +127,8 @@ paths: application/json: schema: $ref: '#/components/schemas/GetFileResponse' + '401': + description: User is not authorized to view the file. security: - Auth query parameter: [] /api/enableSharing: @@ -441,6 +443,8 @@ paths: responses: '200': description: 'The file itself is in the response, as well as an options object.' + '401': + description: User is not authorized to view the file. security: - Auth query parameter: [] /api/deleteFile: @@ -1254,8 +1258,12 @@ components: type: boolean uuid: type: string + uid: + type: string + id: + type: string subscriptionName: - type: boolean + type: string description: Only used for subscriptions subPlaylist: type: boolean diff --git a/src/api-types/models/DownloadFileRequest.ts b/src/api-types/models/DownloadFileRequest.ts index 18c6cac..61816c9 100644 --- a/src/api-types/models/DownloadFileRequest.ts +++ b/src/api-types/models/DownloadFileRequest.ts @@ -11,10 +11,12 @@ export interface DownloadFileRequest { outputName?: string; fullPathProvided?: boolean; uuid?: string; + uid?: string; + id?: string; /** * Only used for subscriptions */ - subscriptionName?: boolean; + subscriptionName?: string; /** * Only used for subscriptions */ diff --git a/src/app/components/custom-playlists/custom-playlists.component.ts b/src/app/components/custom-playlists/custom-playlists.component.ts index d2d65d6..3212708 100644 --- a/src/app/components/custom-playlists/custom-playlists.component.ts +++ b/src/app/components/custom-playlists/custom-playlists.component.ts @@ -71,7 +71,7 @@ export class CustomPlaylistsComponent implements OnInit { } downloadPlaylist(fileNames, type, zipName = null, playlistID = null) { - this.postsService.downloadFileFromServer(fileNames, type, zipName).subscribe(res => { + this.postsService.downloadFileFromServer(fileNames, type, {outputName: zipName}).subscribe(res => { if (playlistID) { this.downloading_content[type][playlistID] = false }; const blob: Blob = res; saveAs(blob, zipName + '.zip'); diff --git a/src/app/components/recent-videos/recent-videos.component.ts b/src/app/components/recent-videos/recent-videos.component.ts index 11a6dcc..b90fee5 100644 --- a/src/app/components/recent-videos/recent-videos.component.ts +++ b/src/app/components/recent-videos/recent-videos.component.ts @@ -190,13 +190,15 @@ export class RecentVideosComponent implements OnInit { const type = (file.isAudio ? 'audio' : 'video') as FileType; const ext = type === 'audio' ? '.mp3' : '.mp4' const sub = this.postsService.getSubscriptionByID(file.sub_id); - this.postsService.downloadFileFromServer(file.id, type, null, null, sub.name, sub.isPlaylist, - this.postsService.user ? this.postsService.user.uid : null, null).subscribe(res => { - const blob: Blob = res; - saveAs(blob, file.id + ext); - }, err => { - console.log(err); - }); + this.postsService.downloadFileFromServer( + file.id, type, + {subscriptionName: sub.name, subPlaylist: sub.isPlaylist, uid: this.postsService.user ? this.postsService.user.uid : null} + ).subscribe(res => { + const blob: Blob = res; + saveAs(blob, file.id + ext); + }, err => { + console.log(err); + }); } downloadNormalFile(file) { diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts index c0bb894..e524057 100644 --- a/src/app/main/main.component.ts +++ b/src/app/main/main.component.ts @@ -773,7 +773,7 @@ export class MainComponent implements OnInit { } downloadPlaylist(fileNames, type, zipName = null, playlistID = null) { - this.postsService.downloadFileFromServer(fileNames, type, zipName).subscribe(res => { + this.postsService.downloadFileFromServer(fileNames, type, {outputName: zipName}).subscribe(res => { if (playlistID) { this.downloading_content[type][playlistID] = false }; const blob: Blob = res; saveAs(blob, zipName + '.zip'); diff --git a/src/app/player/player.component.ts b/src/app/player/player.component.ts index 57e2ea4..c6fb53d 100644 --- a/src/app/player/player.component.ts +++ b/src/app/player/player.component.ts @@ -318,8 +318,10 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { const zipName = fileNames[0].split(' ')[0] + fileNames[1].split(' ')[0]; this.downloading = true; - this.postsService.downloadFileFromServer(fileNames, this.type, zipName, null, null, null, null, - !this.uuid ? this.postsService.user.uid : this.uuid, this.id).subscribe(res => { + this.postsService.downloadFileFromServer( + fileNames, this.type, + {outputName: zipName, uuid: !this.uuid ? this.postsService.user.uid : this.uuid, id: this.id} + ).subscribe(res => { this.downloading = false; const blob: Blob = res; saveAs(blob, zipName + '.zip'); @@ -333,8 +335,10 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { const ext = (this.type === 'audio') ? '.mp3' : '.mp4'; const filename = this.playlist[0].title; this.downloading = true; - this.postsService.downloadFileFromServer(filename, this.type, null, null, this.subscriptionName, this.subPlaylist, - this.is_shared ? this.db_file['uid'] : null, this.uuid).subscribe(res => { + this.postsService.downloadFileFromServer( + filename, this.type, + {subscriptionName: this.subscriptionName, subPlaylist: this.subPlaylist, uid: this.is_shared ? this.db_file['uid'] : null, uuid: this.uuid} + ).subscribe(res => { this.downloading = false; const blob: Blob = res; saveAs(blob, filename + ext); diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 976298c..be2f39e 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -282,8 +282,14 @@ export class PostsService implements CanActivate { return this.http.post(this.path + 'getAllFiles', {}, this.httpOptions); } - downloadFileFromServer(fileName: string | string[], type: FileType, outputName: string = null, fullPathProvided: boolean = null, subscriptionName: boolean = null, subPlaylist: boolean = null, - uid = null, uuid: string = null, id = null) { + downloadFileFromServer( + fileName: string | string[], type: FileType, + options: {outputName?: string, fullPathProvided?: boolean, subscriptionName?: string, subPlaylist?: boolean, uid?: string, uuid?: string, id?: string} = {} + ) { + const {outputName = null, fullPathProvided = null, + subscriptionName = null, subPlaylist = null, + uid = null, uuid = null, id = null} = options; + const body: DownloadFileRequest = {fileNames: fileName, type: type, zip_mode: Array.isArray(fileName), @@ -291,11 +297,11 @@ export class PostsService implements CanActivate { fullPathProvided: fullPathProvided, subscriptionName: subscriptionName, subPlaylist: subPlaylist, + uid: uid, uuid: uuid, id: id, }; - return this.http.post(this.path + 'downloadFile', body, - {responseType: 'blob', params: this.httpOptions.params}); + return this.http.post(this.path + 'downloadFile', body, {responseType: 'blob', params: this.httpOptions.params}); } uploadCookiesFile(fileFormData) { diff --git a/src/app/subscription/subscription/subscription.component.ts b/src/app/subscription/subscription/subscription.component.ts index eb2f35b..2c8f9dd 100644 --- a/src/app/subscription/subscription/subscription.component.ts +++ b/src/app/subscription/subscription/subscription.component.ts @@ -152,7 +152,7 @@ export class SubscriptionComponent implements OnInit { } this.downloading = true; - this.postsService.downloadFileFromServer(fileNames, 'video' as FileType, this.subscription.name, true).subscribe(res => { + this.postsService.downloadFileFromServer(fileNames, 'video' as FileType, {outputName: this.subscription.name, fullPathProvided: true}).subscribe(res => { this.downloading = false; const blob: Blob = res; saveAs(blob, this.subscription.name + '.zip'); From 389c5b5df327b2054478a33632406af6470aae64 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Sat, 26 Sep 2020 14:50:39 -0700 Subject: [PATCH 009/680] Take out import type --- .../components/custom-playlists/custom-playlists.component.ts | 2 +- src/app/components/recent-videos/recent-videos.component.ts | 2 +- .../update-progress-dialog/update-progress-dialog.component.ts | 2 +- src/app/main/main.component.ts | 2 +- src/app/player/player.component.ts | 2 +- src/app/posts.services.ts | 2 +- src/app/subscription/subscription/subscription.component.ts | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/components/custom-playlists/custom-playlists.component.ts b/src/app/components/custom-playlists/custom-playlists.component.ts index 3212708..d4fd013 100644 --- a/src/app/components/custom-playlists/custom-playlists.component.ts +++ b/src/app/components/custom-playlists/custom-playlists.component.ts @@ -91,7 +91,7 @@ export class CustomPlaylistsComponent implements OnInit { this.getAllPlaylists(); }); } - + editPlaylistDialog(args) { const playlist = args.playlist; const index = args.index; diff --git a/src/app/components/recent-videos/recent-videos.component.ts b/src/app/components/recent-videos/recent-videos.component.ts index b90fee5..2bbd96b 100644 --- a/src/app/components/recent-videos/recent-videos.component.ts +++ b/src/app/components/recent-videos/recent-videos.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { PostsService } from 'app/posts.services'; import { Router } from '@angular/router'; -import type { FileType } from '../../../api-types'; +import { FileType } from '../../../api-types'; @Component({ selector: 'app-recent-videos', diff --git a/src/app/dialogs/update-progress-dialog/update-progress-dialog.component.ts b/src/app/dialogs/update-progress-dialog/update-progress-dialog.component.ts index 9ca066f..4862839 100644 --- a/src/app/dialogs/update-progress-dialog/update-progress-dialog.component.ts +++ b/src/app/dialogs/update-progress-dialog/update-progress-dialog.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { PostsService } from 'app/posts.services'; import { MatSnackBar } from '@angular/material/snack-bar'; -import type { UpdaterStatus } from '../../../api-types'; +import { UpdaterStatus } from '../../../api-types'; @Component({ selector: 'app-update-progress-dialog', diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts index e524057..376d573 100644 --- a/src/app/main/main.component.ts +++ b/src/app/main/main.component.ts @@ -20,7 +20,7 @@ import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.com import { Platform } from '@angular/cdk/platform'; import { v4 as uuid } from 'uuid'; import { ArgModifierDialogComponent } from 'app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component'; -import type { FileType } from '../../api-types'; +import { FileType } from '../../api-types'; export let audioFilesMouseHovering = false; export let videoFilesMouseHovering = false; diff --git a/src/app/player/player.component.ts b/src/app/player/player.component.ts index c6fb53d..ca845c1 100644 --- a/src/app/player/player.component.ts +++ b/src/app/player/player.component.ts @@ -7,7 +7,7 @@ import { MatSnackBar } from '@angular/material/snack-bar'; import { InputDialogComponent } from 'app/input-dialog/input-dialog.component'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { ShareMediaDialogComponent } from '../dialogs/share-media-dialog/share-media-dialog.component'; -import type { FileType } from '../../api-types'; +import { FileType } from '../../api-types'; export interface IMedia { title: string; diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index be2f39e..8258ef7 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -10,7 +10,7 @@ import { DOCUMENT } from '@angular/common'; import { BehaviorSubject } from 'rxjs'; import { MatSnackBar } from '@angular/material/snack-bar'; import * as Fingerprint2 from 'fingerprintjs2'; -import type { +import { ChangeRolePermissionsRequest, ChangeUserPermissionsRequest, ConfigResponse, diff --git a/src/app/subscription/subscription/subscription.component.ts b/src/app/subscription/subscription/subscription.component.ts index 2c8f9dd..ffbbde5 100644 --- a/src/app/subscription/subscription/subscription.component.ts +++ b/src/app/subscription/subscription/subscription.component.ts @@ -3,7 +3,7 @@ import { PostsService } from 'app/posts.services'; import { ActivatedRoute, Router, ParamMap } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; import { EditSubscriptionDialogComponent } from 'app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component'; -import type { FileType } from '../../../api-types'; +import { FileType } from '../../../api-types'; @Component({ selector: 'app-subscription', From 2cf0c61facd04d3311b64fd33eeea8e456d906f8 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Sat, 26 Sep 2020 14:53:51 -0700 Subject: [PATCH 010/680] Default booleans to false --- src/app/posts.services.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 8258ef7..66c8a38 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -286,8 +286,8 @@ export class PostsService implements CanActivate { fileName: string | string[], type: FileType, options: {outputName?: string, fullPathProvided?: boolean, subscriptionName?: string, subPlaylist?: boolean, uid?: string, uuid?: string, id?: string} = {} ) { - const {outputName = null, fullPathProvided = null, - subscriptionName = null, subPlaylist = null, + const {outputName = null, fullPathProvided = false, + subscriptionName = null, subPlaylist = false, uid = null, uuid = null, id = null} = options; const body: DownloadFileRequest = {fileNames: fileName, From b0cb09309d67a8f4bc33437b33b70f47d2210d51 Mon Sep 17 00:00:00 2001 From: Florian Gabsteiger Date: Fri, 25 Dec 2020 18:21:05 +0100 Subject: [PATCH 011/680] Fix release asset name creation The complete git ref name was used as part of the release asset filename for tagged commits. This includes the refs/tags prefix, which fails as "/" characters can't be part of filenames. To work around this, a step is added that extracts the pure tag name first. --- .github/workflows/build.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9eb71be..db12f7b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,17 +69,20 @@ jobs: with: name: youtubedl-material path: ${{runner.temp}}/youtubedl-material + - name: extract tag name + id: tag_name + run: echo ::set-output name=tag_name::${GITHUB_REF#refs/tags/} - name: prepare release asset shell: pwsh - run: Compress-Archive -Path ${{runner.temp}}/youtubedl-material -DestinationPath youtubedl-material-${{ github.ref }}.zip + run: Compress-Archive -Path ${{runner.temp}}/youtubedl-material -DestinationPath youtubedl-material-${{ steps.tag_name.outputs.tag_name }}.zip - name: upload build asset uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./youtubedl-material-${{ github.ref }}.zip - asset_name: youtubedl-material-${{ github.ref }}.zip + asset_path: ./youtubedl-material-${{ steps.tag_name.outputs.tag_name }}.zip + asset_name: youtubedl-material-${{ steps.tag_name.outputs.tag_name }}.zip asset_content_type: application/zip - name: upload docker-compose asset uses: actions/upload-release-asset@v1 From 8e4e0c790855ffa4ed5932ed265c64a5f5b6ce4b Mon Sep 17 00:00:00 2001 From: Florian Gabsteiger Date: Fri, 25 Dec 2020 18:32:32 +0100 Subject: [PATCH 012/680] fix wrongly named ci step --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db12f7b..d3b911e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,7 +75,7 @@ jobs: - name: prepare release asset shell: pwsh run: Compress-Archive -Path ${{runner.temp}}/youtubedl-material -DestinationPath youtubedl-material-${{ steps.tag_name.outputs.tag_name }}.zip - - name: upload build asset + - name: upload release asset uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From b6c09324d93f924481441fdebabcec6cd9d8d70c Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Fri, 1 Jan 2021 17:29:50 -0500 Subject: [PATCH 013/680] Updated error messages to make them more verbose and fixed ID3 tagging for file names --- backend/app.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/app.js b/backend/app.js index b9f3fab..6f51c45 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1215,7 +1215,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) { title: output_json['title'], artist: output_json['artist'] ? output_json['artist'] : output_json['uploader'] } - let success = NodeID3.write(tags, output_json['_filename']); + let success = NodeID3.write(tags, utils.removeFileExtension(output_json['_filename']) + '.mp3'); if (!success) logger.error('Failed to apply ID3 tag to audio file ' + output_json['_filename']); } @@ -1535,6 +1535,9 @@ async function getVideoInfoByURL(url, args = [], download = null) { resolve(output); } else { logger.error(`Error while retrieving info on video with URL ${url} with the following message: ${err}`); + if (err.stderr) { + logger.error(`${err.stderr}`) + } if (download) { download['error'] = `Failed pre-check for video info: ${err}`; updateDownloads(); @@ -1562,7 +1565,7 @@ async function getUrlInfos(urls) { let difference = (new_date - startDate)/1000; logger.debug(`URL info retrieval delay: ${difference} seconds.`); if (err) { - logger.error('Error during parsing:' + err); + logger.error(`Error during parsing: ${err}`); resolve(null); } let try_putput = null; From c09dd7a03ba9f7393b7dac28e96720e3576430ae Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Fri, 1 Jan 2021 17:38:08 -0500 Subject: [PATCH 014/680] Updated Chinese and Spanish translations and added Italian translations --- src/app/settings/settings.component.ts | 2 +- src/assets/i18n/messages.es.json | 28 +- src/assets/i18n/messages.es.xlf | 2261 +++++++++++++-------- src/assets/i18n/messages.it.json | 249 +++ src/assets/i18n/messages.it.xlf | 2482 +++++++++++++++++++++++ src/assets/i18n/messages.zh.json | 33 +- src/assets/i18n/messages.zh.xlf | 2578 ++++++++++++++++++++++++ 7 files changed, 6835 insertions(+), 798 deletions(-) create mode 100644 src/assets/i18n/messages.it.json create mode 100644 src/assets/i18n/messages.it.xlf create mode 100644 src/assets/i18n/messages.zh.xlf diff --git a/src/app/settings/settings.component.ts b/src/app/settings/settings.component.ts index 224a1b2..824b270 100644 --- a/src/app/settings/settings.component.ts +++ b/src/app/settings/settings.component.ts @@ -20,7 +20,7 @@ import { EditCategoryDialogComponent } from 'app/dialogs/edit-category-dialog/ed }) export class SettingsComponent implements OnInit { all_locales = isoLangs; - supported_locales = ['en', 'es', 'de', 'fr', 'zh', 'nb', 'en-GB']; + supported_locales = ['en', 'es', 'de', 'fr', 'zh', 'nb', 'it', 'en-GB']; initialLocale = localStorage.getItem('locale'); initial_config = null; diff --git a/src/assets/i18n/messages.es.json b/src/assets/i18n/messages.es.json index 91585e4..c7e3277 100644 --- a/src/assets/i18n/messages.es.json +++ b/src/assets/i18n/messages.es.json @@ -241,5 +241,31 @@ "3697f8583ea42868aa269489ad366103d94aece7": "Editando", "0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "Nivel de registro", "a8b7b9c168fd936a75e500806a8c0d7755ef1198": "NOTA: La carga de cookies nuevas anulará las cookies anteriores y las cookies son para toda la instancia, no por usuario.", - "511b600ae4cf037e4eb3b7a58410842cd5727490": "Agregar más contenido" + "511b600ae4cf037e4eb3b7a58410842cd5727490": "Agregar más contenido", + "56a2a773fbd5a6b9ac2e6b89d29d70a2ed0f3227": "Ver menos.", + "ddc31f2885b1b33a7651963254b0c197f2a64086": "Ver más.", + "c776eb4992b6c98f58cd89b20c1ea8ac37888521": "Seleccione un agente de descarga", + "5fb1e0083c9b2a40ac8ae7dcb2618311c291b8b9": "Descarga automática de Twitch Chat", + "84ffcebac2709ca0785f4a1d5ba274433b5beabc": "También conocido como ID de cliente.", + "8ae23bc4302a479f687f4b20a84c276182e2519c": "Clave de API de Twitch", + "d162f9fcd6a7187b391e004f072ab3da8377c47d": "Usar la API de Twitch", + "04201f9d27abd7d6f58a4328ab98063ce1072006": "Categorías", + "ef418d4ece7c844f3a5e431da1aa59bedd88da7b": "Args personalizados globales", + "1148fd45287ff09955b938756bc302042bcb29c7": "La ruta es relativa a las rutas de descarga anteriores. No incluya la extensión.", + "cfe829634b1144bc44b6d38cf5584ea65db9804f": "Salida de archivo predeterminada", + "3d1a47dc18b7bd8b5d9e1eb44b235ed9c4a2b513": "Volver a descargar nuevas cargas", + "13759b09a7f4074ceee8fa2f968f9815fdf63295": "A veces, los videos nuevos se descargan antes de procesarse por completo. Esta configuración significará que los nuevos videos se verificarán para una versión de mayor calidad al día siguiente.", + "dad95154dcef3509b8cc705046061fd24994bbb7": "vistas", + "792dc6a57f28a1066db283f2e736484f066005fd": "Descargar Twitch Chat", + "e4eeb9106dbcbc91ca1ac3fb4068915998a70f37": "Añadir nueva regla", + "2489eefea00931942b91f4a1ae109514b591e2e1": "Reglas", + "c3b0b86523f1d10e84a71f9b188d54913a11af3b": "Editando la categoría", + "07db550ae114d9faad3a0cbb68bcc16ab6cd31fc": "Pausado", + "73423607944a694ce6f9e55cfee329681bb4d9f9": "No se encontraron vídeos.", + "29376982b1205d9d6ea3d289e8e2f8e1ac2839b1": "Orden inverso", + "33026f57ea65cd9c8a5d917a08083f71a718933a": "Orden normal", + "5caadefa4143cf6766a621b0f54f91f373a1f164": "Añadir contenido", + "0cc1dec590ecd74bef71a865fb364779bc42a749": "Categoría:", + "303e45ffae995c9817e510e38cb969e6bb3adcbf": "(Pausado)", + "d641b8fa5ac5e85114c733b1f7de6976bd091f70": "Calidad máxima" } \ No newline at end of file diff --git a/src/assets/i18n/messages.es.xlf b/src/assets/i18n/messages.es.xlf index f116163..04a30ba 100644 --- a/src/assets/i18n/messages.es.xlf +++ b/src/assets/i18n/messages.es.xlf @@ -1,799 +1,1474 @@ - + - - No need to include URL, just everything after. - No es necesario incluir URL, solo todo después - - - Global custom args for downloads on the home page. - Argumentos personalizados globales para descargas en la página de inicio. - - - Create a playlist - Crea una lista de reproducción - - - Name - Nombre - - - Audio files - Archivos de sonido - - - Videos - Archivos de video - - - Modify youtube-dl args - Modificar args de youtube-dl - - - Simulated new args - Args nuevos simulados - - - Add an arg - Añadir un arg - - - Search by category - Busqueda por categoria - - - Use arg value - Usar valor de arg - - - Arg value - Valor de arg - - - Add arg - Añadir arg - - - Cancel - Cancelar - - - Modify - Modificar - - - Youtube Downloader - Descargador de Youtube - - - Please enter a valid URL! - Por favor entre una URL válida - - - Quality - Calidad - - - Use URL - Usa URL - - - View - Ver - - - Only Audio - Solo audio - - - Multi-download Mode - Descarga múltiple - - - Download - Descarga - - - Cancel - Cancelar - - - Advanced - Avanzado - - - Simulated command: - Commando simulado: - - - Use custom args - Usar argumentos personalizados - - - Custom args - Argumentos personalizados - - - No need to include URL, just everything after. Args are delimited using two commas like so: ,, - No es necesario incluir URL, solo todo después. Los argumentos se delimitan usando dos comas así: ,, - - - Use custom output - Usar salida personalizada - - - Custom output - Salida personalizada - - - Documentation - Documentación - - - Path is relative to the config download path. Don't include extension. - La ruta es relativa a la ruta de descarga de la config. No incluya el extensión. - - - Use authentication - Usa autenticación - - - Username - Nombre de usuario - - - Password - Contraseña - - - Audio - Audio - - - Your audio files are here - Tus archivos de audio están aquí - - - Playlists - Listas de reproducción - - - No playlists available. Create one from your downloading audio files by clicking the blue plus button. - No hay listas de reproducción disponibles. Cree uno de tus archivos de audio haciendo clic en el botón azul más. - - - Video - Vídeo - - - Your video files are here - Tus archivos de video son aquí - - - No playlists available. Create one from your downloading video files by clicking the blue plus button. - No hay listas de reproducción disponibles. Cree uno de tus archivos de video haciendo clic en el botón azul más. - - - Name: - Nombre: - - - URL: - URL: - - - Uploader: - Cargador: - - - File size: - Tamaño del archivo: - - - Path: - Ruta: - - - Upload Date: - Subido: - - - Close - Cerca - - - ID: - ID: - - - Count: - Cuenta: - - - Info - Información - - - Delete - Eliminar - - - Delete and blacklist - Eliminar y pones en la lista negra - - - Settings - Configuraciones - - - URL - URL - - - URL this app will be accessed from, without the port. - URL desde la que se accederá a esta aplicación, sin el puerto. - - - Port - Puerto - - - The desired port. Default is 17442. - Puerto deseado. El valor predeterminado es 17442. - - - Multi-user mode - Modo multiusuario - - - Users base path - Ruta base de usuarios - - - Base path for users and their downloaded videos. - Ruta base para los usuarios y sus videos descargados. - - - Use encryption - Usa cifrado - - - Cert file path - Ruta del archivo de certificado - - - Key file path - Ruta de archivo de clave - - - Allow subscriptions - Permitir suscripciones - - - Subscriptions base path - Ruta base de suscripciones - - - Base path for videos from your subscribed channels and playlists. It is relative to YTDL-Material's root folder. - Ruta base para videos de sus canales y listas de reproducción suscritos. Es relativo a la carpeta raíz de YTDL-Material. - - - Check interval - Intervalo de comprobación - - - Unit is seconds, only include numbers. - La unidad es segundos, solo incluye números. - - - Use youtube-dl archive - Usa el archivo de youtube-dl - - - With youtube-dl's archive - Con la función de archivo de youtube-dl, - - - feature, downloaded videos from your subscriptions get recorded in a text file in the subscriptions archive sub-directory. - los videos descargados de sus suscripciones se graban en un archivo de texto en el subdirectorio del archivo de suscripciones. - - - This enables the ability to permanently delete videos from your subscriptions without unsubscribing, and allows you to record which videos you downloaded in case of data loss. - Esto permite eliminar videos de sus suscripciones de forma permanente sin darse de baja y le permite grabar los videos que descargó en caso de pérdida de datos. - - - Theme - Tema - - - Default - Defecto - - - Dark - Oscura - - - Allow theme change - Permitir cambio de tema - - - Language - Idioma - - - Main - Principal - - - Audio folder path - Ruta de la carpeta de audio - - - Path for audio only downloads. It is relative to YTDL-Material's root folder. - Ruta para descargas de solo audio. Es relativo a la carpeta raíz de YTDL-Material. - - - Video folder path - Ruta de la carpeta de video - - - Path for video downloads. It is relative to YTDL-Material's root folder. - Ruta de descarga de videos. Es relativo a la carpeta raíz de YTDL-Material. - - - Global custom args for downloads on the home page. Args are delimited using two commas like so: ,, - Argumentos personalizados globales para descargas en la página de inicio. Los argumentos se delimitan usando dos comas así: ,, - - - Downloader - Descargador - - - Top title - Título superior - - - File manager enabled - Administrador de archivos habilitado - - - Downloads manager enabled - Administrador de descargas habilitado - - - Allow quality select - Permitir selección de calidad - - - Download only mode - Modo de solo descarga - - - Allow multi-download mode - Permitir el modo de descarga múltiple - - - Require pin for settings - Requiere pin para la configuración - - - Set New Pin - Establecer nuevo pin - - - Enable Public API - Habilitar API pública - - - Public API Key - Clave API pública - - - View documentation - Ver documentación - - - Generate - Generar - - - Use YouTube API - Utilizar la API de YouTube - - - Youtube API Key - Clave API de YouTube - - - Generating a key is easy! - ¡Generar una clave es fácil! - - - Click here - ¡Haga clic aquí - - - to download the official YoutubeDL-Material Chrome extension manually. - para descargar la extensión Chrome oficial de YoutubeDL-Material manualmente. - - - You must manually load the extension and modify the extension's settings to set the frontend URL. - Debe cargar manualmente la extensión y modificar la configuración de la extensión para establecer la URL de la interfaz. - - - to install the official YoutubeDL-Material Firefox extension right off the Firefox extensions page. - para instalar la extensión Firefox oficial de YoutubeDL-Material directamente desde la página de extensiones de Firefox. - - - Detailed setup instructions. - Instrucciones detalladas de configuración. - - - Not much is required other than changing the extension's settings to set the frontend URL. - No se requiere mucho más que cambiar la configuración de la extensión para establecer la URL de la interfaz. - - - Drag the link below to your bookmarks, and you're good to go! Just navigate to the YouTube video you'd like to download, and click the bookmark. - Arrastra el enlace de abajo a tus marcadores, ¡y listo! Simplemente navegue hasta el video de YouTube que desea descargar y haga clic en el marcador. - - - Generate 'audio only' bookmarklet - Generar bookmarklet solo de audio - - - Extra - Extra - - - Use default downloading agent - Usar agente de descarga predeterminado - - - Select a downloader - Seleccione un descargador - - - Allow advanced download - Permitir descarga avanzada - - - Advanced - Avanzado - - - Allow user registration - Permitir registro de usuario - - - Users - Usuarios - - - Save - Salvar - - - {VAR_SELECT, select, true {Close} false {Cancel} other {otha} } - {VAR_SELECT, select, true {Cerrar} false {Cancelar} other {Otro} } - - - About YoutubeDL-Material - Sobre YoutubeDL-Material - - - is an open-source YouTube downloader built under Google's Material Design specifications. You can seamlessly download your favorite videos as video or audio files, and even subscribe to your favorite channels and playlists to keep updated with their new videos. - es un descargador de código abierto de YouTube creado bajo las especificaciones de "Material Design" de Google. Puede descargar sin problemas sus videos favoritos como archivos de video o audio, e incluso suscribirse a sus canales favoritos y listas de reproducción para mantenerse actualizado con sus nuevos videos. - - - has some awesome features included! An extensive API, Docker support, and localization (translation) support. Read up on all the supported features by clicking on the GitHub icon above. - tiene algunas características increíbles incluidas! Una amplia API, soporte de Docker y soporte de localización (traducción). Lea todas las funciones compatibles haciendo clic en el icono de GitHub que se encuentra arriba. - - - Installed version: - Versión instalada: - - - Checking for updates... - Comprobando actualizaciones... - - - Update available - Actualización disponible - - - You can update from the settings menu. - Puede actualizar desde el menú de configuración. - - - Found a bug or have a suggestion? - ¿Encontró un error o tiene una sugerencia? - - - to create an issue! - para crear una cuestión! - - - Your Profile - Tu perfil - - - UID: - UID: - - - Created: - Creado: - - - You are not logged in. - Usted no se ha identificado. - - - Login - Identificarse - - - Logout - Salir - - - Create admin account - Crear cuenta de administrador - - - No default admin account detected. This will create and set the password for an admin account with the user name as 'admin'. - No se detectó una cuenta de administrador predeterminada. Esto creará y establecerá la contraseña para una cuenta de administrador con el nombre de usuario como 'admin'. - - - Create - Crear - - - Profile - Perfil - - - About - Sobre - - - Home - Inicio - - - Subscriptions - Suscripciones - - - Downloads - Descargas - - - Share playlist - Compartir lista de reproducción - - - Share video - Compartir vídeo - - - Share audio - Compartir audio - - - Enable sharing - Habilitar compartir - - - Use timestamp - Usar marca de tiempo - - - Seconds - Segundos - - - Copy to clipboard - Copiar al Portapapeles - - - Save changes - Guardar cambios - - - Details - Detalles - - - An error has occured: - Se ha producido un error: - - - Download start: - Inicio de descarga: - - - Download end: - Fin de descarga: - - - File path(s): - Ruta(s) del archivo: - - - Subscribe to playlist or channel - Suscríbase a la lista de reproducción o al canal - - - The playlist or channel URL - La lista de reproducción o la URL del canal - - - Custom name - Nombre personalizado - - - This is optional - Esto es opcional - - - Download all uploads - Descargar todas las cargas - - - Download videos uploaded in the last - Descargar videos subidos en el último - - - Streaming-only mode - Modo de solo transmisión - - - Subscribe - Subscribe - - - Type: - Tipo: - - - Archive: - Archivo: - - - Export Archive - Exportar el archivo - - - Unsubscribe - Darse de baja - - - Your subscriptions - Sus suscripciones - - - Channels - Canales - - - Name not available. Channel retrieval in progress. - Nombre no disponible. Recuperación de canales en progreso. - - - You have no channel subscriptions. - No tienes suscripciones de canal. - - - Name not available. Playlist retrieval in progress. - Nombre no disponible. Recuperación de listas de reproducción en progreso. - - - You have no playlist subscriptions. - No tienes suscripciones a listas de reproducción. - - - Search - Buscar - - - Length: - Duración: - - - Delete and redownload - Eliminar y volver a descargar - - - Delete forever - Borrar para siempre - - - Updater - Updater - - - Select a version: - Seleccione una versión: - - - Register - Registrarse - - - Session ID: - ID de sesión: - - - (current) - (actual) - - - No downloads available! - ¡No hay descargas disponibles! - - - Register a user - Registrar un usuario - - - User name - Nombre de usuario - - - Manage user - Administrar usuario - - - User UID: - UID de usuario: - - - New password - Nueva contraseña - - - Set new password - Establecer nueva contraseña - - - Use default - Uso por defecto - - - Yes - Si - - - No - No - - - Manage role - Gestionar rol - - - User name - Nombre de usuario - - - Role - Rol - - - Actions - Acciones - - - Add Users - Agregar Usuarios - - - Edit Role - Editar Rol - + + No need to include URL, just everything after. + No es necesario incluir URL, solo todo después + + + Global custom args for downloads on the home page. + Argumentos personalizados globales para descargas en la página de inicio. + + + Create a playlist + Crea una lista de reproducción + + + Name + Nombre + + + Audio files + Archivos de sonido + + + Videos + Archivos de video + + + Modify youtube-dl args + Modificar args de youtube-dl + + + Simulated new args + Args nuevos simulados + + + Add an arg + Añadir un arg + + + Search by category + Busqueda por categoria + + + Use arg value + Usar valor de arg + + + Arg value + Valor de arg + + + Add arg + Añadir arg + + + Cancel + Cancelar + + + Modify + Modificar + + + Youtube Downloader + Descargador de Youtube + + + Please enter a valid URL! + Por favor entre una URL válida + + + Quality + Calidad + + + Use URL + Usa URL + + + View + Ver + + + Only Audio + Solo audio + + + Multi-download Mode + Descarga múltiple + + + Download + Descarga + + + Cancel + Cancelar + + + Advanced + Avanzado + + + Simulated command: + Commando simulado: + + + Use custom args + Usar argumentos personalizados + + + Custom args + Argumentos personalizados + + + No need to include URL, just everything after. Args are delimited using two commas like so: ,, + No es necesario incluir URL, solo todo después. Los argumentos se delimitan usando dos comas así: ,, + + + Use custom output + Usar salida personalizada + + + Custom output + Salida personalizada + + + Documentation + Documentación + + + Path is relative to the config download path. Don't include extension. + La ruta es relativa a la ruta de descarga de la config. No incluya el extensión. + + + Use authentication + Usa autenticación + + + Username + Nombre de usuario + + + Password + Contraseña + + + Audio + Audio + + + Your audio files are here + Tus archivos de audio están aquí + + + Playlists + Listas de reproducción + + + No playlists available. Create one from your downloading audio files by clicking the blue plus button. + No hay listas de reproducción disponibles. Cree uno de tus archivos de audio haciendo clic en el botón azul más. + + + Video + Vídeo + + + Your video files are here + Tus archivos de video son aquí + + + No playlists available. Create one from your downloading video files by clicking the blue plus button. + No hay listas de reproducción disponibles. Cree uno de tus archivos de video haciendo clic en el botón azul más. + + + Name: + Nombre: + + + URL: + URL: + + + Uploader: + Cargador: + + + File size: + Tamaño del archivo: + + + Path: + Ruta: + + + Upload Date: + Subido: + + + Close + Cerca + + + ID: + ID: + + + Count: + Cuenta: + + + Info + Información + + + Delete + Eliminar + + + Delete and blacklist + Eliminar y pones en la lista negra + + + Settings + Configuraciones + + + URL + URL + + + URL this app will be accessed from, without the port. + URL desde la que se accederá a esta aplicación, sin el puerto. + + + Port + Puerto + + + The desired port. Default is 17442. + Puerto deseado. El valor predeterminado es 17442. + + + Multi-user mode + Modo multiusuario + + + Users base path + Ruta base de usuarios + + + Base path for users and their downloaded videos. + Ruta base para los usuarios y sus videos descargados. + + + Use encryption + Usa cifrado + + + Cert file path + Ruta del archivo de certificado + + + Key file path + Ruta de archivo de clave + + + Allow subscriptions + Permitir suscripciones + + + Subscriptions base path + Ruta base de suscripciones + + + Base path for videos from your subscribed channels and playlists. It is relative to YTDL-Material's root folder. + Ruta base para videos de sus canales y listas de reproducción suscritos. Es relativo a la carpeta raíz de YTDL-Material. + + + Check interval + Intervalo de comprobación + + + Unit is seconds, only include numbers. + La unidad es segundos, solo incluye números. + + + Use youtube-dl archive + Usa el archivo de youtube-dl + + + With youtube-dl's archive + Con la función de archivo de youtube-dl, + + + feature, downloaded videos from your subscriptions get recorded in a text file in the subscriptions archive sub-directory. + los videos descargados de sus suscripciones se graban en un archivo de texto en el subdirectorio del archivo de suscripciones. + + + This enables the ability to permanently delete videos from your subscriptions without unsubscribing, and allows you to record which videos you downloaded in case of data loss. + Esto permite eliminar videos de sus suscripciones de forma permanente sin darse de baja y le permite grabar los videos que descargó en caso de pérdida de datos. + + + Theme + Tema + + + Default + Defecto + + + Dark + Oscura + + + Allow theme change + Permitir cambio de tema + + + Language + Idioma + + + Main + Principal + + + Audio folder path + Ruta de la carpeta de audio + + + Path for audio only downloads. It is relative to YTDL-Material's root folder. + Ruta para descargas de solo audio. Es relativo a la carpeta raíz de YTDL-Material. + + + Video folder path + Ruta de la carpeta de video + + + Path for video downloads. It is relative to YTDL-Material's root folder. + Ruta de descarga de videos. Es relativo a la carpeta raíz de YTDL-Material. + + + Global custom args for downloads on the home page. Args are delimited using two commas like so: ,, + Argumentos personalizados globales para descargas en la página de inicio. Los argumentos se delimitan usando dos comas así: ,, + + + Downloader + Descargador + + + Top title + Título superior + + + File manager enabled + Administrador de archivos habilitado + + + Downloads manager enabled + Administrador de descargas habilitado + + + Allow quality select + Permitir selección de calidad + + + Download only mode + Modo de solo descarga + + + Allow multi-download mode + Permitir el modo de descarga múltiple + + + Require pin for settings + Requiere pin para la configuración + + + Set New Pin + Establecer nuevo pin + + + Enable Public API + Habilitar API pública + + + Public API Key + Clave API pública + + + View documentation + Ver documentación + + + Generate + Generar + + + Use YouTube API + Utilizar la API de YouTube + + + Youtube API Key + Clave API de YouTube + + + Generating a key is easy! + ¡Generar una clave es fácil! + + + Click here + ¡Haga clic aquí + + + to download the official YoutubeDL-Material Chrome extension manually. + para descargar la extensión Chrome oficial de YoutubeDL-Material manualmente. + + + You must manually load the extension and modify the extension's settings to set the frontend URL. + Debe cargar manualmente la extensión y modificar la configuración de la extensión para establecer la URL de la interfaz. + + + to install the official YoutubeDL-Material Firefox extension right off the Firefox extensions page. + para instalar la extensión Firefox oficial de YoutubeDL-Material directamente desde la página de extensiones de Firefox. + + + Detailed setup instructions. + Instrucciones detalladas de configuración. + + + Not much is required other than changing the extension's settings to set the frontend URL. + No se requiere mucho más que cambiar la configuración de la extensión para establecer la URL de la interfaz. + + + Drag the link below to your bookmarks, and you're good to go! Just navigate to the YouTube video you'd like to download, and click the bookmark. + Arrastra el enlace de abajo a tus marcadores, ¡y listo! Simplemente navegue hasta el video de YouTube que desea descargar y haga clic en el marcador. + + + Generate 'audio only' bookmarklet + Generar bookmarklet solo de audio + + + Extra + Extra + + + Use default downloading agent + Usar agente de descarga predeterminado + + + Select a downloader + Seleccione un descargador + + + Allow advanced download + Permitir descarga avanzada + + + Advanced + Avanzado + + + Allow user registration + Permitir registro de usuario + + + Users + Usuarios + + + Save + Salvar + + + {VAR_SELECT, select, true {Close} false {Cancel} other {otha} } + {VAR_SELECT, select, true {Cerrar} false {Cancelar} other {Otro} } + + + About YoutubeDL-Material + Sobre YoutubeDL-Material + + + is an open-source YouTube downloader built under Google's Material Design specifications. You can seamlessly download your favorite videos as video or audio files, and even subscribe to your favorite channels and playlists to keep updated with their new videos. + es un descargador de código abierto de YouTube creado bajo las especificaciones de "Material Design" de Google. Puede descargar sin problemas sus videos favoritos como archivos de video o audio, e incluso suscribirse a sus canales favoritos y listas de reproducción para mantenerse actualizado con sus nuevos videos. + + + has some awesome features included! An extensive API, Docker support, and localization (translation) support. Read up on all the supported features by clicking on the GitHub icon above. + tiene algunas características increíbles incluidas! Una amplia API, soporte de Docker y soporte de localización (traducción). Lea todas las funciones compatibles haciendo clic en el icono de GitHub que se encuentra arriba. + + + Installed version: + Versión instalada: + + + Checking for updates... + Comprobando actualizaciones... + + + Update available + Actualización disponible + + + You can update from the settings menu. + Puede actualizar desde el menú de configuración. + + + Found a bug or have a suggestion? + ¿Encontró un error o tiene una sugerencia? + + + to create an issue! + para crear una cuestión! + + + Your Profile + Tu perfil + + + UID: + UID: + + + Created: + Creado: + + + You are not logged in. + Usted no se ha identificado. + + + Login + Identificarse + + + Logout + Salir + + + Create admin account + Crear cuenta de administrador + + + No default admin account detected. This will create and set the password for an admin account with the user name as 'admin'. + No se detectó una cuenta de administrador predeterminada. Esto creará y establecerá la contraseña para una cuenta de administrador con el nombre de usuario como 'admin'. + + + Create + Crear + + + Profile + Perfil + + + About + Sobre + + + Home + Inicio + + + Subscriptions + Suscripciones + + + Downloads + Descargas + + + Share playlist + Compartir lista de reproducción + + + Share video + Compartir vídeo + + + Share audio + Compartir audio + + + Enable sharing + Habilitar compartir + + + Use timestamp + Usar marca de tiempo + + + Seconds + Segundos + + + Copy to clipboard + Copiar al Portapapeles + + + Save changes + Guardar cambios + + + Details + Detalles + + + An error has occured: + Se ha producido un error: + + + Download start: + Inicio de descarga: + + + Download end: + Fin de descarga: + + + File path(s): + Ruta(s) del archivo: + + + Subscribe to playlist or channel + Suscríbase a la lista de reproducción o al canal + + + The playlist or channel URL + La lista de reproducción o la URL del canal + + + Custom name + Nombre personalizado + + + This is optional + Esto es opcional + + + Download all uploads + Descargar todas las cargas + + + Download videos uploaded in the last + Descargar videos subidos en el último + + + Streaming-only mode + Modo de solo transmisión + + + Subscribe + Subscribe + + + Type: + Tipo: + + + Archive: + Archivo: + + + Export Archive + Exportar el archivo + + + Unsubscribe + Darse de baja + + + Your subscriptions + Sus suscripciones + + + Channels + Canales + + + Name not available. Channel retrieval in progress. + Nombre no disponible. Recuperación de canales en progreso. + + + You have no channel subscriptions. + No tienes suscripciones de canal. + + + Name not available. Playlist retrieval in progress. + Nombre no disponible. Recuperación de listas de reproducción en progreso. + + + You have no playlist subscriptions. + No tienes suscripciones a listas de reproducción. + + + Search + Buscar + + + Length: + Duración: + + + Delete and redownload + Eliminar y volver a descargar + + + Delete forever + Borrar para siempre + + + Updater + Updater + + + Select a version: + Seleccione una versión: + + + Register + Registrarse + + + Session ID: + ID de sesión: + + + (current) + (actual) + + + No downloads available! + ¡No hay descargas disponibles! + + + Register a user + Registrar un usuario + + + User name + Nombre de usuario + + + Manage user + Administrar usuario + + + User UID: + UID de usuario: + + + New password + Nueva contraseña + + + Set new password + Establecer nueva contraseña + + + Use default + Uso por defecto + + + Yes + Si + + + No + No + + + Manage role + Gestionar rol + + + User name + Nombre de usuario + + + Role + Rol + + + Actions + Acciones + + + Add Users + Agregar Usuarios + + + Edit Role + Editar Rol + + + Logs + Registros + + app/settings/settings.component.html + 346 + + Logs settings label + + + Search Base + Search Base + + app/settings/settings.component.html + 333 + + Search Base + + + Search Filter + Search Filter + + app/settings/settings.component.html + 338 + + Search Filter + + + Bind Credentials + Bind Credentials + + app/settings/settings.component.html + 328 + + Bind Credentials + + + Bind DN + Bind DN + + app/settings/settings.component.html + 323 + + Bind DN + + + LDAP URL + URL LDAP + + app/settings/settings.component.html + 318 + + LDAP URL + + + Auth method + Método de autenticación + + app/settings/settings.component.html + 306 + + Auth method select + + + LDAP + LDAP + + app/settings/settings.component.html + 311 + + LDAP auth method + + + Internal + Interno + + app/settings/settings.component.html + 308 + + Internal auth method + + + Set Cookies + Configurar Cookies + + app/settings/settings.component.html + 288 + + Set cookies button + + + Use Cookies + Usar Cookies + + app/settings/settings.component.html + 287 + + Use cookies setting + + + Login expiration + Caducidad de inicio de sesión + + app/settings/settings.component.html + 268 + + Login expiration select label + + + Select a logger level + Seleccione un nivel de registrador + + app/settings/settings.component.html + 256 + + Logger level select label + + + This will delete your old API key! + ¡Esto eliminará su vieja clave API! + + app/settings/settings.component.html + 187 + + delete api key tooltip + + + Kill all downloads + Mata todas las descargas + + app/settings/settings.component.html + 139 + + Kill all downloads button + + + Include metadata + Incluir metadatos + + app/settings/settings.component.html + 135 + + Include metadata setting + + + Include thumbnail + Incluir miniatura + + app/settings/settings.component.html + 131 + + Include thumbnail setting + + + NOTE: Uploading new cookies will overrride your previous cookies. Also note that cookies are instance-wide, not per-user. + NOTA: La carga de cookies nuevas anulará las cookies anteriores y las cookies son para toda la instancia, no por usuario. + + app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html + 20 + + Cookies upload warning + + + Drag and Drop + Arrastrar y soltar + + app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html + 11 + + Drag and Drop + + + Upload new cookies + Subir nuevas cookies + + app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html + 1 + + Cookies uploader dialog title + + + Edit + Editar + + app/file-card/file-card.component.html + 19 + + + app/components/unified-file-card/unified-file-card.component.html + 32 + + Playlist edit button + + + Modify playlist + Modificar lista de reproducción + + app/dialogs/modify-playlist/modify-playlist.component.html + 1 + + Modify playlist dialog title + + + Type + Tipo + + app/create-playlist/create-playlist.component.html + 11 + + Type select + + + Video + Vídeo + + app/create-playlist/create-playlist.component.html + 13 + + Video + + + Audio + Audio + + app/create-playlist/create-playlist.component.html + 12 + + Audio + + + My videos + Mis videos + + app/components/recent-videos/recent-videos.component.html + 20 + + My videos title + + + Go to subscription + Ir a suscripción + + app/components/unified-file-card/unified-file-card.component.html + 20 + + Go to subscription menu item + + + Open file in new tab + Abrir archivo en nueva pestaña + + app/components/unified-file-card/unified-file-card.component.html + 14 + + Open file in new tab + + + Open file + Abrir archivo + + app/components/unified-file-card/unified-file-card.component.html + 13 + + Open file button + + + Clear logs + Borrar registros + + app/components/logs-viewer/logs-viewer.component.html + 34 + + Clear logs button + + + Lines: + Líneas: + + app/components/logs-viewer/logs-viewer.component.html + 22 + + Label for lines select in logger view + + + Delete user + Eliminar usuario + + app/components/modify-users/modify-users.component.html + 73 + + delete user action button tooltip + + + Edit user + Editar usuario + + app/components/modify-users/modify-users.component.html + 66 + + edit user action button tooltip + + + Custom file output + Salida de archivo personalizado + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 59 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 43 + + Subscription custom file output placeholder + + + These are added after the standard args. + Estos se agregan después de los argumentos estándar. + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 53 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 37 + + Custom args hint + + + Audio-only mode + Modo de solo audio + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 40 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 24 + + Streaming-only mode + + + An error has occurred: + Un error ha ocurrido: + + app/download-item/download-item.component.html + 27 + + Error label + + + An error has occurred + Un error ha ocurrido + + app/download-item/download-item.component.html + 9 + + download error tooltip + + + The download was successful + La descarga era exitosa + + app/download-item/download-item.component.html + 8 + + download successful tooltip + + + Use role default + Usar rol predeterminado + + app/components/manage-user/manage-user.component.html + 19 + + Use role default + + + Clear all downloads + Claro todas las descargas + + app/components/downloads/downloads.component.html + 18 + + clear all downloads action button + + + Editing + Editando + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 1 + + Edit subscription dialog title prefix + + + Log Level + Nivel de registro + + app/settings/settings.component.html + 256 + + Log Level label + + + NOTE: Uploading new cookies will override your previous cookies. Also note that cookies are instance-wide, not per-user. + NOTA: La carga de cookies nuevas anulará las cookies anteriores y las cookies son para toda la instancia, no por usuario. + + app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html + 20 + + Cookies upload warning + + + Add more content + Agregar más contenido + + app/dialogs/modify-playlist/modify-playlist.component.html + 17 + + Add more content + + + See less. + Ver menos. + + src/app/components/see-more/see-more.component.html + 8,9 + + See less + + + See more. + Ver más. + + src/app/components/see-more/see-more.component.html + 5,6 + + See more + + + Select a download agent + Seleccione un agente de descarga + + src/app/settings/settings.component.html + 299 + + Custom downloader select label + + + Auto-download Twitch Chat + Descarga automática de Twitch Chat + + src/app/settings/settings.component.html + 244 + + Auto download Twitch Chat setting + + + Also known as a Client ID. + También conocido como ID de cliente. + + src/app/settings/settings.component.html + 249 + + Twitch API Key setting hint AKA preamble + + + Twitch API Key + Clave de API de Twitch + + src/app/settings/settings.component.html + 248 + + Twitch API Key setting placeholder + + + Use Twitch API + Usar la API de Twitch + + src/app/settings/settings.component.html + 241 + + Use Twitch API setting + + + Categories + Categorías + + src/app/settings/settings.component.html + 144 + + Categories + + + Global custom args + Args personalizados globales + + src/app/settings/settings.component.html + 133 + + Custom args input placeholder + + + Path is relative to the above download paths. Don't include extension. + La ruta es relativa a las rutas de descarga anteriores. No incluya la extensión. + + src/app/settings/settings.component.html + 126 + + Custom Output input hint + + + Default file output + Salida de archivo predeterminada + + src/app/settings/settings.component.html + 123 + + Default file output placeholder + + + Redownload fresh uploads + Volver a descargar nuevas cargas + + src/app/settings/settings.component.html + 63 + + Redownload fresh uploads + + + Sometimes new videos are downloaded before being fully processed. This setting will mean new videos will be checked for a higher quality version the following day. + A veces, los videos nuevos se descargan antes de procesarse por completo. Esta configuración significará que los nuevos videos se verificarán para una versión de mayor calidad al día siguiente. + + src/app/settings/settings.component.html + 63 + + Redownload fresh uploads tooltip + + + views + vistas + + src/app/player/player.component.html + 15 + + View count label + + + Download Twitch Chat + Descargar Twitch Chat + + src/app/components/twitch-chat/twitch-chat.component.html + 10 + + Download Twitch Chat button + + + Add new rule + Añadir nueva regla + + src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html + 39 + + Add new rule tooltip + + + Rules + Reglas + + src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html + 10 + + Rules + + + Editing category + Editando la categoría + + src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html + 1 + + Editing category dialog title + + + Paused + Pausado + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 7 + + Paused subscription setting + + + No videos found. + No se encontraron vídeos. + + src/app/components/recent-videos/recent-videos.component.html + 38 + + No videos found + + + Reverse order + Orden inverso + + src/app/dialogs/modify-playlist/modify-playlist.component.html + 14 + + Reverse order + + + Normal order + Orden normal + + src/app/dialogs/modify-playlist/modify-playlist.component.html + 13 + + Normal order + + + Add content + Añadir contenido + + src/app/dialogs/modify-playlist/modify-playlist.component.html + 19 + + Add content + + + Category: + Categoría: + + src/app/dialogs/video-info-dialog/video-info-dialog.component.html + 29 + + Category property + + + (Paused) + (Pausado) + + src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 1 + + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 1 + + + src/app/subscriptions/subscriptions.component.html + 12 + + + src/app/subscriptions/subscriptions.component.html + 31 + + + src/app/subscription/subscription/subscription.component.html + 5 + + Paused suffix + + + Max quality + Calidad máxima + + src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 40 + + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 32 + + Max quality placeholder + diff --git a/src/assets/i18n/messages.it.json b/src/assets/i18n/messages.it.json new file mode 100644 index 0000000..8a79d82 --- /dev/null +++ b/src/assets/i18n/messages.it.json @@ -0,0 +1,249 @@ +{ + "17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Crea una scaletta", + "cff1428d10d59d14e45edec3c735a27b5482db59": "Nome", + "f0baeb8b69d120073b6d60d34785889b0c3232c8": "Audio", + "2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "Video", + "f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "Tipo", + "f47e2d56dd8a145b2e9599da9730c049d52962a2": "File audio", + "a52dae09be10ca3a65da918533ced3d3f4992238": "Video", + "d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Modifica i parametri di youtube-dl", + "7fc1946abe2b40f60059c6cd19975d677095fd19": "Simula i nuovi parametri", + "0b71824ae71972f236039bed43f8d2323e8fd570": "Aggiungi un'impostazione", + "c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Cerca per categoria", + "9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Usa valore impostato", + "25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Valore impostato", + "7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Aggiungi impostazione", + "d7b35c384aecd25a516200d6921836374613dfe7": "Annulla", + "b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Modifica", + "a38ae1082fec79ba1f379978337385a539a28e73": "Qualità", + "4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "Utilizza URL", + "d3f02f845e62cebd75fde451ab8479d2a8ad784d": "Visualizza", + "4a9889d36910edc8323d7bab60858ab3da6d91df": "Solo audio", + "96a01fafe135afc58b0f8071a4ab00234495ce18": "Modalità download multiplo", + "6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Scarica", + "6a3777f913cf3f288664f0632b9f24794fdcc24e": "Annulla", + "322ed150e02666fe2259c5b4614eac7066f4ffa0": "Avanzato", + "b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Comando simulato:", + "4e4c721129466be9c3862294dc40241b64045998": "Usa parametri personalizzati", + "ad2f8ac8b7de7945b80c8e424484da94e597125f": "Parametri personalizzati", + "a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "Non è necessario includere l'URL, solo ciò che viene dopo. I parametri sono delimitati da due vigole: ,,", + "3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Usa output personalizzato", + "d9c02face477f2f9cdaae318ccee5f89856851fb": "Output personalizzata", + "fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Documentazione", + "19d1ae64d94d28a29b2c57ae8671aace906b5401": "Il percorso è relativo al percorso di download configurato. Non includere l'estensione.", + "8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "Utilizza l'autenticazione", + "08c74dc9762957593b91f6eb5d65efdfc975bf48": "Nome utente", + "c32ef07f8803a223a83ed17024b38e8d82292407": "Password", + "616e206cb4f25bd5885fc35925365e43cf5fb929": "Nome:", + "c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL:", + "c6eb45d085384903e53ab001a3513d1de6a1dbac": "Caricato da:", + "109c6f4a5e46efb933612ededfaf52a13178b7e0": "Dimensioni file:", + "bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Percorso:", + "a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Data di caricamento:", + "f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Chiudi", + "4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Modifica playlist", + "511b600ae4cf037e4eb3b7a58410842cd5727490": "Aggiungi più contenuto", + "52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Salva", + "ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID:", + "e684046d73bcee88e82f7ff01e2852789a05fc32": "Conteggio:", + "28f86ffd419b869711aa13f5e5ff54be6d70731c": "Modifica", + "826b25211922a1b46436589233cb6f1a163d89b7": "Elimina", + "321e4419a943044e674beb55b8039f42a9761ca5": "Informazioni", + "34504b488c24c27e68089be549f0eeae6ebaf30b": "Elimina e aggiungi alla lista nera", + "ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Carica nuovi cookie", + "98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Trascina e rilascia", + "a8b7b9c168fd936a75e500806a8c0d7755ef1198": "NOTA: il caricamento di nuovi cookie sovrascriverà i cookie precedenti. Inoltre tieni presente che i cookie sono a livello di processo, non per utente.", + "121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Impostazioni", + "801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL", + "54c512cca1923ab72faf1a0bd98d3d172469629a": "URL con cui si accederà a questa applicazione, senza porta.", + "cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Porta", + "22e8f1d0423a3b784fe40fab187b92c06541b577": "Porta personalizzata. La predefinita è 17442.", + "d4477669a560750d2064051a510ef4d7679e2f3e": "Modalità multiutente", + "2eb03565fcdce7a7a67abc277a936a32fcf51557": "Percorso profili utente", + "a64505c41150663968e277ec9b3ddaa5f4838798": "Percorso per profili utente e per video scaricati da ognuno.", + "4e3120311801c4acd18de7146add2ee4a4417773": "Consenti iscrizioni", + "4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Percorso salvataggio playlist sottoscritte", + "bc9892814ee2d119ae94378c905ea440a249b84a": "Percorso salvataggio per i video dei canali e delle playlist sottoscritte. È relativo alla cartella principale di YTDL-Material.", + "5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Intervallo di verifica", + "0f56a7449b77630c114615395bbda4cab398efd8": "Unità in secondi, inserire solo numeri.", + "27a56aad79d8b61269ed303f11664cc78bcc2522": "Tema", + "ff7cee38a2259526c519f878e71b964f41db4348": "Predefinito", + "adb4562d2dbd3584370e44496969d58c511ecb63": "Scuro", + "7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Consenti variazione tema", + "fe46ccaae902ce974e2441abe752399288298619": "Lingua", + "82421c3e46a0453a70c42900eab51d58d79e6599": "Principale", + "ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Percorso della cartella audio", + "c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Percorso per download solo audio. È relativo alla cartella principale di YTDL-Material.", + "46826331da1949bd6fb74624447057099c9d20cd": "Percorso cartella Video", + "17c92e6d47a213fa95b5aa344b3f258147123f93": "Percorso per il download di video. È relativo alla cartella principale di YTDL-Material.", + "6b995e7130b4d667eaab6c5f61b362ace486d26d": "Parametri personalizzati generali per i download sulla home page. I parametri sono delimitati da due virgole: ,,", + "78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Usa l'archivio youtube-dl", + "ffc19f32b1cba0daefc0e5668f89346db1db83ad": "Includi anteprima", + "384de8f8f112c9e6092eb2698706d391553f3e8d": "Includi metadati", + "fb35145bfb84521e21b6385363d59221f436a573": "Interrompi tutti i download", + "0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Scaricato da", + "61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Titolo della barra superiore", + "78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Abilita il file manager", + "a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Abilita il download manager", + "c33bd5392b39dbed36b8e5a1145163a15d45835f": "Consenti la selezione della qualità", + "bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Modalità solo download", + "09d31c803a7252658694e1e3176b97f5655a3fe3": "Consenti la modalità di download multiplo", + "1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Abilita l'API Pubblica", + "23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Chiave API Pubblica", + "41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Visualizza la documentazione", + "1b258b258b4cc475ceb2871305b61756b0134f4a": "Genera", + "00a94f58d9eb2e3aa561440eabea616d0c937fa2": "Cancellerai la tua chiave API precedente!", + "d5d7c61349f3b0859336066e6d453fc35d334fe5": "Usa l'API di YouTube", + "ce10d31febb3d9d60c160750570310f303a22c22": "Chiave API YouTube", + "8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "Generare una chiave è facile!", + "9b3cedfa83c6d7acb3210953289d1be4aab115c7": "Premi qui", + "7f09776373995003161235c0c8d02b7f91dbc4df": "per scaricare manualmente l'estensione ufficiale per Chrome di YoutubeDL-Material.", + "5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "È necessario installare manualmente l'estensione e modificarne le impostazioni inserendo l'URL della pagina principale.", + "9a2ec6da48771128384887525bdcac992632c863": "per installare l'estensione ufficiale YoutubeDL-Material per Firefox direttamente dalla pagina delle estensioni di Firefox.", + "eb81be6b49e195e5307811d1d08a19259d411f37": "Istruzioni dettagliate per la configurazione.", + "cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "Viene richiesto semplicemente di modificare le impostazioni dell'estensione, inserendo l'URL della pagina principale.", + "61b81b11aad0b9d970ece2fce18405f07eac69c2": "Trascina il link qui sotto tra tuoi preferiti e sei a posto! Vai al video YouTube che desideri scaricare e fai clic sul preferito.", + "c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "Genera un preferito \"solo audio\"", + "d5f69691f9f05711633128b5a3db696783266b58": "Extra", + "5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Usa agente di download predefinito", + "ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Seleziona un metodo di download", + "0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "Livello di Log", + "db6c192032f4cab809aad35215f0aa4765761897": "Scadenza accesso", + "dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Consenti download avanzato", + "431e5f3a0dde88768d1074baedd65266412b3f02": "Usa i cookie", + "80651a7ad1229ea6613557d3559f702cfa5aecf5": "Imposta i cookie", + "bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Avanzate", + "37224420db54d4bc7696f157b779a7225f03ca9d": "Consenti registrazione utente", + "4f56ced9d6b85aeb1d4346433361d47ea72dac1a": "Interno", + "e3d7c5f019e79a3235a28ba24df24f11712c7627": "LDAP", + "fa548cee6ea11c160a416cac3e6bdec0363883dc": "Metodo di autenticazione", + "1db9789b93069861019bd0ccaa5d4706b00afc61": "URL LDAP", + "f50fa6c09c8944aed504f6325f2913ee6c7a296a": "Associa DN", + "080cc6abcba236390fc22e79792d0d3443a3bd2a": "Associa credenziali", + "cfa67d14d84fe0e9fadf251dc51ffc181173b662": "Base di ricerca", + "e01d54ecc1a0fcf9525a3c100ed8b83d94e61c23": "Filtro di ricerca", + "4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Utenti", + "eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Registri", + "fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, seleziona, vero {Close} falso {Cancel} altro {otha}}", + "cec82c0a545f37420d55a9b6c45c20546e82f94e": "Informazioni su YoutubeDL-Material", + "199c17e5d6a419313af3c325f06dcbb9645ca618": "è un downloader di YouTube open source costruito secondo le specifiche Material Design di Google. Puoi scaricare agevolmente i tuoi video preferiti come file video o solo audio e persino iscriverti ai tuoi canali e playlist preferiti per tenerti aggiornato con i nuovi video pubblicati.", + "bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "ha alcune fantastiche funzionalità incluse! Una solida API, supporto Docker e supporto per la localizzazione (traduzione). Leggi tutte le funzionalità supportate cliccando sull'icona GitHub in alto.", + "a45e3b05f0529dc5246d70ef62304c94426d4c81": "Versione installata:", + "e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Verifica aggiornamenti in corso...", + "a16e92385b4fd9677bb830a4b796b8b79c113290": "Aggiornamento disponibile", + "189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "È possibile eseguire l'aggiornamento dal menù impostazioni.", + "b33536f59b94ec935a16bd6869d836895dc5300c": "Hai trovato un errore o hai un suggerimento?", + "e1f398f38ff1534303d4bb80bd6cece245f24016": "per segnalare un problema!", + "42ff677ec14f111e88bd6cdd30145378e994d1bf": "Il tuo profilo", + "ac9d09de42edca1296371e4d801349c9096ac8de": "UID:", + "a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Creato:", + "fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Non hai eseguito l'accesso.", + "6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Accedi", + "bb694b49d408265c91c62799c2b3a7e3151c824d": "Esci", + "a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Crea un account amministratore", + "2d2adf3ca26a676bca2269295b7455a26fd26980": "Nessun account amministratore predefinito rilevato. Verrà creato e impostata la password per un account amministratore con il nome utente \"admin\".", + "70a67e04629f6d412db0a12d51820b480788d795": "Crea", + "994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Profilo", + "004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Informazioni", + "92eee6be6de0b11c924e3ab27db30257159c0a7c": "Pagina principale", + "357064ca9d9ac859eb618e28e8126fa32be049e2": "Iscrizioni", + "822fab38216f64e8166d368b59fe756ca39d301b": "Download", + "a249a5ae13e0835383885aaf697d2890cc3e53e9": "Condividi playlist", + "15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Condividi il video", + "1d540dcd271b316545d070f9d182c372d923aadd": "Condividi l'audio", + "1f6d14a780a37a97899dc611881e6bc971268285": "Abilita la condivisione", + "6580b6a950d952df847cb3d8e7176720a740adc8": "Usa data e ora", + "4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "Secondi", + "3a6e5a6aa78ca864f6542410c5dafb6334538106": "Copia negli appunti", + "5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Salva le modifiche", + "4d8a18b04a1f785ecd8021ac824e0dfd5881dbfc": "Il download è riuscito", + "348cc5d553b18e862eb1c1770e5636f6b05ba130": "Si è verificato un errore", + "4f8b2bb476981727ab34ed40fde1218361f92c45": "Dettagli", + "e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "Si è verificato un errore:", + "77b0c73840665945b25bd128709aa64c8f017e1c": "Inizio download:", + "08ff9375ec078065bcdd7637b7ea65fce2979266": "Termine download:", + "ad127117f9471612f47d01eae09709da444a36a4": "Percorso(i) file:", + "a9806cf78ce00eb2613eeca11354a97e033377b8": "Iscriviti alla playlist o al canale", + "93efc99ae087fc116de708ecd3ace86ca237cf30": "URL della playlist o del canale", + "08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Nome personalizzato", + "ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Scarica tutti i file caricati", + "28a678e9cabf86e44c32594c43fa0e890135c20f": "Scarica i video caricati negli ultimi", + "c76a955642714b8949ff3e4b4990864a2e2cac95": "Modalità solo audio", + "408ca4911457e84a348cecf214f02c69289aa8f1": "Modalità solo streaming", + "f432e1a8d6adb12e612127978ce2e0ced933959c": "Questi vengono aggiunti dopo ai parametri standard.", + "98b6ec9ec138186d663e64770267b67334353d63": "File di uscita personalizzato", + "d0336848b0c375a1c25ba369b3481ee383217a4f": "Iscriviti", + "e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Tipo:", + "a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Archivio:", + "8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Esporta archivio", + "3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "Annulla l'iscrizione", + "e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "I tuoi abbonamenti", + "807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Canali", + "29b89f751593e1b347eef103891b7a1ff36ec03f": "Nome non disponibile. Recupero del canale in corso.", + "4636cd4a1379c50d471e98786098c4d39e1e82ad": "Non sei iscritto a nessun canale.", + "47546e45bbb476baaaad38244db444c427ddc502": "Scalette", + "2e0a410652cb07d069f576b61eab32586a18320d": "Nome non disponibile. Recupero playlist in corso.", + "587b57ced54965d8874c3fd0e9dfedb987e5df04": "Non sei iscritto a nessuna playlist.", + "3697f8583ea42868aa269489ad366103d94aece7": "Modifica", + "7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Cerca", + "2054791b822475aeaea95c0119113de3200f5e1c": "Durata:", + "94e01842dcee90531caa52e4147f70679bac87fe": "Elimina e scarica di nuovo", + "2031adb51e07a41844e8ba7704b054e98345c9c1": "Elimina definitivamente", + "91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Aggiornato da", + "1372e61c5bd06100844bd43b98b016aabc468f62": "Seleziona una versione:", + "cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registrati", + "a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "ID sessione:", + "eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(attuale)", + "b6c453e0e61faea184bbaf5c5b0a1e164f4de2a2": "Cancella tutti i download", + "7117fc42f860e86d983bfccfcf2654e5750f3406": "Nessun download disponibile!", + "b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Registra un utente", + "024886ca34a6f309e3e51c2ed849320592c3faaa": "Nome utente", + "2bd201aea09e43fbfd3cd15ec0499b6755302329": "Gestisci utente", + "29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "UID utente:", + "e70e209561583f360b1e9cefd2cbb1fe434b6229": "Nuova password", + "6498fa1b8f563988f769654a75411bb8060134b9": "Imposta nuova password", + "544e09cdc99a8978f48521d45f62db0da6dcf742": "Usa il ruolo predefinito", + "4f20f2d5a6882190892e58b85f6ccbedfa737952": "Sì", + "3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "No", + "57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Gestisci ruolo", + "746f64ddd9001ac456327cd9a3d5152203a4b93c": "Nome utente", + "52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Ruolo", + "59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Azioni", + "632e8b20c98e8eec4059a605a4b011bb476137af": "Modifica utente", + "95b95a9c79e4fd9ed41f6855e37b3b06af25bcab": "Elimina utente", + "4d92a0395dd66778a931460118626c5794a3fc7a": "Aggiungi utenti", + "b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Modifica ruolo", + "5009630cdf32ab4f1c78737b9617b8773512c05a": "Linee:", + "8a0bda4c47f10b2423ff183acefbf70d4ab52ea2": "Cancella i registri", + "ccf5ea825526ac490974336cb5c24352886abc07": "Apri il file", + "5656a06f17c24b2d7eae9c221567b209743829a9": "Apri il file in una nuova scheda", + "a0720c36ee1057e5c54a86591b722485c62d7b1a": "Vai alle iscrizioni", + "d02888c485d3aeab6de628508f4a00312a722894": "I miei video", + "ef418d4ece7c844f3a5e431da1aa59bedd88da7b": "Parametri personalizzati generali", + "56a2a773fbd5a6b9ac2e6b89d29d70a2ed0f3227": "Nascondi.", + "84ffcebac2709ca0785f4a1d5ba274433b5beabc": "Conosciuto anche come Client ID.", + "cfe829634b1144bc44b6d38cf5584ea65db9804f": "File output predefinito", + "3d1a47dc18b7bd8b5d9e1eb44b235ed9c4a2b513": "Riscarica i nuovi contenuti", + "13759b09a7f4074ceee8fa2f968f9815fdf63295": "A volte i nuovi video vengono scaricati prima di essere completamente elaborati. Questa impostazione significa che per i nuovi video verrà effettuata una verifica il giorno successivo per la ricerca di versioni di qualità superiore.", + "ddc31f2885b1b33a7651963254b0c197f2a64086": "Vedi altro.", + "24dc3ecf7ec2c2144910c4f3d38343828be03a4c": "Generato automaticamente", + "c776eb4992b6c98f58cd89b20c1ea8ac37888521": "Seleziona un agente di download", + "5fb1e0083c9b2a40ac8ae7dcb2618311c291b8b9": "Scarica automaticamente le Chat Twitch", + "8ae23bc4302a479f687f4b20a84c276182e2519c": "Chiave dell'API Twitch", + "d162f9fcd6a7187b391e004f072ab3da8377c47d": "Usa l'API Twitch", + "04201f9d27abd7d6f58a4328ab98063ce1072006": "Categorie", + "1148fd45287ff09955b938756bc302042bcb29c7": "Il percorso è riferito ai percorsi di download sopra. Non includere l'estensione.", + "dad95154dcef3509b8cc705046061fd24994bbb7": "visualizzazioni", + "792dc6a57f28a1066db283f2e736484f066005fd": "Scarica Chat Twitch", + "e4eeb9106dbcbc91ca1ac3fb4068915998a70f37": "Aggiungi nuova regola", + "2489eefea00931942b91f4a1ae109514b591e2e1": "Regole", + "c3b0b86523f1d10e84a71f9b188d54913a11af3b": "Categoria in modifica", + "07db550ae114d9faad3a0cbb68bcc16ab6cd31fc": "In pausa", + "73423607944a694ce6f9e55cfee329681bb4d9f9": "Nessun video trovato.", + "29376982b1205d9d6ea3d289e8e2f8e1ac2839b1": "Ordine inverso", + "33026f57ea65cd9c8a5d917a08083f71a718933a": "Ordine normale", + "5caadefa4143cf6766a621b0f54f91f373a1f164": "Aggiungi contenuto", + "0cc1dec590ecd74bef71a865fb364779bc42a749": "Categoria:", + "303e45ffae995c9817e510e38cb969e6bb3adcbf": "(In pausa)", + "d641b8fa5ac5e85114c733b1f7de6976bd091f70": "Qualità massima" +} \ No newline at end of file diff --git a/src/assets/i18n/messages.it.xlf b/src/assets/i18n/messages.it.xlf new file mode 100644 index 0000000..9f86229 --- /dev/null +++ b/src/assets/i18n/messages.it.xlf @@ -0,0 +1,2482 @@ + + + + + + Create a playlist + Crea una scaletta + + app/create-playlist/create-playlist.component.html + 1 + + Create a playlist dialog title + + + Name + Nome + + app/create-playlist/create-playlist.component.html + 6 + + + app/dialogs/modify-playlist/modify-playlist.component.html + 7 + + Playlist name placeholder + + + Audio + Audio + + app/create-playlist/create-playlist.component.html + 12 + + Audio + + + Video + Video + + app/create-playlist/create-playlist.component.html + 13 + + Video + + + Type + Tipo + + app/create-playlist/create-playlist.component.html + 11 + + Type select + + + Audio files + File audio + + app/create-playlist/create-playlist.component.html + 19 + + Audio files title + + + Videos + Video + + app/create-playlist/create-playlist.component.html + 20 + + + app/subscription/subscription/subscription.component.html + 28 + + Videos title + + + Modify youtube-dl args + Modifica i parametri di youtube-dl + + app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 1 + + Modify args title + + + Simulated new args + Simula i nuovi parametri + + app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 8 + + Simulated args title + + + Add an arg + Aggiungi un'impostazione + + app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 34 + + Add arg card title + + + Search by category + Cerca per categoria + + app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 60 + + Search args by category button + + + Use arg value + Usa valore impostato + + app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 64 + + Use arg value checkbox + + + Arg value + Valore impostato + + app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 68 + + Arg value placeholder + + + Add arg + Aggiungi impostazione + + app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 73 + + Search args by category button + + + Cancel + Annulla + + app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 84 + + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 72 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 56 + + + app/components/modify-users/modify-users.component.html + 61 + + Arg modifier cancel button + + + Modify + Modifica + + app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 85 + + Arg modifier modify button + + + Quality + Qualità + + app/main/main.component.html + 18 + + Quality select label + + + Use URL + Utilizza URL + + app/main/main.component.html + 46 + + YT search Use URL button for searched video + + + View + Visualizza + + app/main/main.component.html + 49 + + YT search View button for searched video + + + Only Audio + Solo audio + + app/main/main.component.html + 59 + + Only Audio checkbox + + + Multi-download Mode + Modalità download multiplo + + app/main/main.component.html + 64 + + Multi-download Mode checkbox + + + Download + Scarica + + app/main/main.component.html + 73 + + Main download button + + + Cancel + Annulla + + app/main/main.component.html + 78 + + Cancel download button + + + Advanced + Avanzato + + app/main/main.component.html + 90 + + Advanced download mode panel + + + Simulated command: + Comando simulato: + + app/main/main.component.html + 96 + + Simulated command label + + + Use custom args + Usa parametri personalizzati + + app/main/main.component.html + 104 + + Use custom args checkbox + + + Custom args + Parametri personalizzati + + app/main/main.component.html + 110 + + + app/settings/settings.component.html + 120 + + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 50 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 34 + + Custom args placeholder + + + No need to include URL, just everything after. Args are delimited using two commas like so: ,, + Non è necessario includere l'URL, solo ciò che viene dopo. I parametri sono delimitati da due vigole: ,, + + app/main/main.component.html + 112 + + Custom Args input hint + + + Use custom output + Usa output personalizzato + + app/main/main.component.html + 120 + + Use custom output checkbox + + + Custom output + Output personalizzata + + app/main/main.component.html + 125 + + Custom output placeholder + + + Documentation + Documentazione + + app/main/main.component.html + 127 + + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 62 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 46 + + Youtube-dl output template documentation link + + + Path is relative to the config download path. Don't include extension. + Il percorso è relativo al percorso di download configurato. Non includere l'estensione. + + app/main/main.component.html + 128 + + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 63 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 47 + + Custom Output input hint + + + Use authentication + Utilizza l'autenticazione + + app/main/main.component.html + 134 + + Use authentication checkbox + + + Username + Nome utente + + app/main/main.component.html + 139 + + YT Username placeholder + + + Password + Password + + app/main/main.component.html + 144 + + + app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html + 10 + + + app/dialogs/add-user-dialog/add-user-dialog.component.html + 11 + + YT Password placeholder + + + Name: + Nome: + + app/dialogs/video-info-dialog/video-info-dialog.component.html + 5 + + + app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 6 + + Video name property + + + URL: + URL: + + app/dialogs/video-info-dialog/video-info-dialog.component.html + 9 + + + app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 9 + + Video URL property + + + Uploader: + Caricato da: + + app/dialogs/video-info-dialog/video-info-dialog.component.html + 13 + + Video ID property + + + File size: + Dimensioni file: + + app/dialogs/video-info-dialog/video-info-dialog.component.html + 17 + + Video file size property + + + Path: + Percorso: + + app/dialogs/video-info-dialog/video-info-dialog.component.html + 21 + + Video path property + + + Upload Date: + Data di caricamento: + + app/dialogs/video-info-dialog/video-info-dialog.component.html + 25 + + Video upload date property + + + Close + Chiudi + + app/dialogs/video-info-dialog/video-info-dialog.component.html + 31 + + + app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html + 40 + + + app/dialogs/about-dialog/about-dialog.component.html + 59 + + + app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 27 + + + app/dialogs/share-media-dialog/share-media-dialog.component.html + 30 + + + app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 23 + + + app/dialogs/update-progress-dialog/update-progress-dialog.component.html + 17 + + + app/dialogs/add-user-dialog/add-user-dialog.component.html + 18 + + + app/components/manage-user/manage-user.component.html + 30 + + + app/components/manage-role/manage-role.component.html + 18 + + Close subscription info button + + + Modify playlist + Modifica playlist + + app/dialogs/modify-playlist/modify-playlist.component.html + 1 + + Modify playlist dialog title + + + Add more content + Aggiungi più contenuto + + app/dialogs/modify-playlist/modify-playlist.component.html + 17 + + Add more content + + + Save + Salva + + app/dialogs/modify-playlist/modify-playlist.component.html + 27 + + + app/settings/settings.component.html + 359 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 58 + + + app/components/modify-users/modify-users.component.html + 58 + + Save + + + ID: + ID: + + app/file-card/file-card.component.html + 7 + + + app/download-item/download-item.component.html + 4 + + + app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 13 + + File or playlist ID + + + Count: + Conteggio: + + app/file-card/file-card.component.html + 8 + + Playlist video count + + + Edit + Modifica + + app/file-card/file-card.component.html + 19 + + + app/components/unified-file-card/unified-file-card.component.html + 32 + + Playlist edit button + + + Delete + Elimina + + app/file-card/file-card.component.html + 20 + + + app/file-card/file-card.component.html + 25 + + + app/components/unified-file-card/unified-file-card.component.html + 28 + + + app/components/unified-file-card/unified-file-card.component.html + 34 + + Delete playlist + + + Info + Informazioni + + app/file-card/file-card.component.html + 24 + + + app/subscription/subscription-file-card/subscription-file-card.component.html + 7 + + + app/components/unified-file-card/unified-file-card.component.html + 19 + + Video info button + + + Delete and blacklist + Elimina e aggiungi alla lista nera + + app/file-card/file-card.component.html + 26 + + + app/components/unified-file-card/unified-file-card.component.html + 29 + + Delete and blacklist video button + + + Upload new cookies + Carica nuovi cookie + + app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html + 1 + + Cookies uploader dialog title + + + Drag and Drop + Trascina e rilascia + + app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html + 11 + + Drag and Drop + + + NOTE: Uploading new cookies will override your previous cookies. Also note that cookies are instance-wide, not per-user. + NOTA: il caricamento di nuovi cookie sovrascriverà i cookie precedenti. Inoltre tieni presente che i cookie sono a livello di processo, non per utente. + + app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html + 20 + + Cookies upload warning + + + Settings + Impostazioni + + app/settings/settings.component.html + 1 + + + app/app.component.html + 28 + + Settings title + + + URL + URL + + app/settings/settings.component.html + 18 + + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 8 + + URL input placeholder + + + URL this app will be accessed from, without the port. + URL con cui si accederà a questa applicazione, senza porta. + + app/settings/settings.component.html + 19 + + URL setting input hint + + + Port + Porta + + app/settings/settings.component.html + 24 + + Port input placeholder + + + The desired port. Default is 17442. + Porta personalizzata. La predefinita è 17442. + + app/settings/settings.component.html + 25 + + Port setting input hint + + + Multi-user mode + Modalità multiutente + + app/settings/settings.component.html + 34 + + Multi user mode setting + + + Users base path + Percorso profili utente + + app/settings/settings.component.html + 38 + + Users base path placeholder + + + Base path for users and their downloaded videos. + Percorso per profili utente e per video scaricati da ognuno. + + app/settings/settings.component.html + 39 + + Users base path hint + + + Allow subscriptions + Consenti iscrizioni + + app/settings/settings.component.html + 48 + + Allow subscriptions setting + + + Subscriptions base path + Percorso salvataggio playlist sottoscritte + + app/settings/settings.component.html + 52 + + Subscriptions base path input setting placeholder + + + Base path for videos from your subscribed channels and playlists. It is relative to YTDL-Material's root folder. + Percorso salvataggio per i video dei canali e delle playlist sottoscritte. È relativo alla cartella principale di YTDL-Material. + + app/settings/settings.component.html + 53 + + Subscriptions base path setting input hint + + + Check interval + Intervallo di verifica + + app/settings/settings.component.html + 58 + + Check interval input setting placeholder + + + Unit is seconds, only include numbers. + Unità in secondi, inserire solo numeri. + + app/settings/settings.component.html + 59 + + Check interval setting input hint + + + Theme + Tema + + app/settings/settings.component.html + 69 + + Theme select label + + + Default + Predefinito + + app/settings/settings.component.html + 71 + + Default theme label + + + Dark + Scuro + + app/settings/settings.component.html + 72 + + + app/app.component.html + 23 + + Dark theme label + + + Allow theme change + Consenti variazione tema + + app/settings/settings.component.html + 77 + + Allow theme change setting + + + Language + Lingua + + app/settings/settings.component.html + 86 + + Language select label + + + Main + Principale + + app/settings/settings.component.html + 12 + + Main settings label + + + Audio folder path + Percorso della cartella audio + + app/settings/settings.component.html + 106 + + Audio folder path input placeholder + + + Path for audio only downloads. It is relative to YTDL-Material's root folder. + Percorso per download solo audio. È relativo alla cartella principale di YTDL-Material. + + app/settings/settings.component.html + 107 + + Aduio path setting input hint + + + Video folder path + Percorso cartella Video + + app/settings/settings.component.html + 113 + + Video folder path input placeholder + + + Path for video downloads. It is relative to YTDL-Material's root folder. + Percorso per il download di video. È relativo alla cartella principale di YTDL-Material. + + app/settings/settings.component.html + 114 + + Video path setting input hint + + + Global custom args for downloads on the home page. Args are delimited using two commas like so: ,, + Parametri personalizzati generali per i download sulla home page. I parametri sono delimitati da due virgole: ,, + + app/settings/settings.component.html + 121 + + Custom args setting input hint + + + Use youtube-dl archive + Usa l'archivio youtube-dl + + app/settings/settings.component.html + 127 + + Use youtubedl archive setting + + + Include thumbnail + Includi anteprima + + app/settings/settings.component.html + 131 + + Include thumbnail setting + + + Include metadata + Includi metadati + + app/settings/settings.component.html + 135 + + Include metadata setting + + + Kill all downloads + Interrompi tutti i download + + app/settings/settings.component.html + 139 + + Kill all downloads button + + + Downloader + Scaricato da + + app/settings/settings.component.html + 99 + + Downloader settings label + + + Top title + Titolo della barra superiore + + app/settings/settings.component.html + 152 + + Top title input placeholder + + + File manager enabled + Abilita il file manager + + app/settings/settings.component.html + 157 + + File manager enabled setting + + + Downloads manager enabled + Abilita il download manager + + app/settings/settings.component.html + 160 + + Downloads manager enabled setting + + + Allow quality select + Consenti la selezione della qualità + + app/settings/settings.component.html + 163 + + Allow quality seelct setting + + + Download only mode + Modalità solo download + + app/settings/settings.component.html + 166 + + Download only mode setting + + + Allow multi-download mode + Consenti la modalità di download multiplo + + app/settings/settings.component.html + 169 + + Allow multi-download mode setting + + + Enable Public API + Abilita l'API Pubblica + + app/settings/settings.component.html + 177 + + Enable Public API key setting + + + Public API Key + Chiave API Pubblica + + app/settings/settings.component.html + 182 + + Public API Key setting placeholder + + + View documentation + Visualizza la documentazione + + app/settings/settings.component.html + 183 + + View API docs setting hint + + + Generate + Genera + + app/settings/settings.component.html + 187 + + Generate key button + + + This will delete your old API key! + Cancellerai la tua chiave API precedente! + + app/settings/settings.component.html + 187 + + delete api key tooltip + + + Use YouTube API + Usa l'API di YouTube + + app/settings/settings.component.html + 196 + + Use YouTube API setting + + + Youtube API Key + Chiave API YouTube + + app/settings/settings.component.html + 200 + + Youtube API Key setting placeholder + + + Generating a key is easy! + Generare una chiave è facile! + + app/settings/settings.component.html + 201 + + Youtube API Key setting hint + + + Click here + Premi qui + + app/settings/settings.component.html + 211 + + + app/settings/settings.component.html + 217 + + + app/dialogs/about-dialog/about-dialog.component.html + 25 + + Chrome ext click here + + + to download the official YoutubeDL-Material Chrome extension manually. + per scaricare manualmente l'estensione ufficiale per Chrome di YoutubeDL-Material. + + app/settings/settings.component.html + 211 + + Chrome click here suffix + + + You must manually load the extension and modify the extension's settings to set the frontend URL. + È necessario installare manualmente l'estensione e modificarne le impostazioni inserendo l'URL della pagina principale. + + app/settings/settings.component.html + 212 + + Chrome setup suffix + + + to install the official YoutubeDL-Material Firefox extension right off the Firefox extensions page. + per installare l'estensione ufficiale YoutubeDL-Material per Firefox direttamente dalla pagina delle estensioni di Firefox. + + app/settings/settings.component.html + 217 + + Firefox click here suffix + + + Detailed setup instructions. + Istruzioni dettagliate per la configurazione. + + app/settings/settings.component.html + 218 + + Firefox setup prefix link + + + Not much is required other than changing the extension's settings to set the frontend URL. + Viene richiesto semplicemente di modificare le impostazioni dell'estensione, inserendo l'URL della pagina principale. + + app/settings/settings.component.html + 218 + + Firefox setup suffix + + + Drag the link below to your bookmarks, and you're good to go! Just navigate to the YouTube video you'd like to download, and click the bookmark. + Trascina il link qui sotto tra tuoi preferiti e sei a posto! Vai al video YouTube che desideri scaricare e fai clic sul preferito. + + app/settings/settings.component.html + 223 + + Bookmarklet instructions + + + Generate 'audio only' bookmarklet + Genera un preferito "solo audio" + + app/settings/settings.component.html + 224 + + Generate audio only bookmarklet checkbox + + + Extra + Extra + + app/settings/settings.component.html + 146 + + Extra settings label + + + Use default downloading agent + Usa agente di download predefinito + + app/settings/settings.component.html + 238 + + Use default downloading agent setting + + + Select a downloader + Seleziona un metodo di download + + app/settings/settings.component.html + 242 + + Custom downloader select label + + + Log Level + Livello di Log + + app/settings/settings.component.html + 256 + + Log Level label + + + Login expiration + Scadenza accesso + + app/settings/settings.component.html + 268 + + Login expiration select label + + + Allow advanced download + Consenti download avanzato + + app/settings/settings.component.html + 279 + + Allow advanced downloading setting + + + Use Cookies + Usa i cookie + + app/settings/settings.component.html + 287 + + Use cookies setting + + + Set Cookies + Imposta i cookie + + app/settings/settings.component.html + 288 + + Set cookies button + + + Advanced + Avanzate + + app/settings/settings.component.html + 233 + + Host settings label + + + Allow user registration + Consenti registrazione utente + + app/settings/settings.component.html + 302 + + Allow registration setting + + + Internal + Interno + + app/settings/settings.component.html + 308 + + Internal auth method + + + LDAP + LDAP + + app/settings/settings.component.html + 311 + + LDAP auth method + + + Auth method + Metodo di autenticazione + + app/settings/settings.component.html + 306 + + Auth method select + + + LDAP URL + URL LDAP + + app/settings/settings.component.html + 318 + + LDAP URL + + + Bind DN + Associa DN + + app/settings/settings.component.html + 323 + + Bind DN + + + Bind Credentials + Associa credenziali + + app/settings/settings.component.html + 328 + + Bind Credentials + + + Search Base + Base di ricerca + + app/settings/settings.component.html + 333 + + Search Base + + + Search Filter + Filtro di ricerca + + app/settings/settings.component.html + 338 + + Search Filter + + + Users + Utenti + + app/settings/settings.component.html + 298 + + Users settings label + + + Logs + Registri + + app/settings/settings.component.html + 346 + + Logs settings label + + + {VAR_SELECT, select, true {Close} false {Cancel} other {otha}} + {VAR_SELECT, seleziona, vero {Close} falso {Cancel} altro {otha}} + + app/settings/settings.component.html + 362 + + Settings cancel and close button + + + About YoutubeDL-Material + Informazioni su YoutubeDL-Material + + app/dialogs/about-dialog/about-dialog.component.html + 1 + + About dialog title + + + is an open-source YouTube downloader built under Google's Material Design specifications. You can seamlessly download your favorite videos as video or audio files, and even subscribe to your favorite channels and playlists to keep updated with their new videos. + è un downloader di YouTube open source costruito secondo le specifiche Material Design di Google. Puoi scaricare agevolmente i tuoi video preferiti come file video o solo audio e persino iscriverti ai tuoi canali e playlist preferiti per tenerti aggiornato con i nuovi video pubblicati. + + app/dialogs/about-dialog/about-dialog.component.html + 12 + + About first paragraph + + + has some awesome features included! An extensive API, Docker support, and localization (translation) support. Read up on all the supported features by clicking on the GitHub icon above. + ha alcune fantastiche funzionalità incluse! Una solida API, supporto Docker e supporto per la localizzazione (traduzione). Leggi tutte le funzionalità supportate cliccando sull'icona GitHub in alto. + + app/dialogs/about-dialog/about-dialog.component.html + 15 + + About second paragraph + + + Installed version: + Versione installata: + + app/dialogs/about-dialog/about-dialog.component.html + 20 + + Version label + + + Checking for updates... + Verifica aggiornamenti in corso... + + app/dialogs/about-dialog/about-dialog.component.html + 20 + + Checking for updates text + + + Update available + Aggiornamento disponibile + + app/dialogs/about-dialog/about-dialog.component.html + 21 + + View latest update + + + You can update from the settings menu. + È possibile eseguire l'aggiornamento dal menù impostazioni. + + app/dialogs/about-dialog/about-dialog.component.html + 21 + + Update through settings menu hint + + + Found a bug or have a suggestion? + Hai trovato un errore o hai un suggerimento? + + app/dialogs/about-dialog/about-dialog.component.html + 25 + + About bug prefix + + + to create an issue! + per segnalare un problema! + + app/dialogs/about-dialog/about-dialog.component.html + 25 + + About bug suffix + + + Your Profile + Il tuo profilo + + app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 1 + + User profile dialog title + + + UID: + UID: + + app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 9 + + UID + + + Created: + Creato: + + app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 12 + + Created + + + You are not logged in. + Non hai eseguito l'accesso. + + app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 19 + + Not logged in notification + + + Login + Accedi + + app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 20 + + + app/app.component.html + 44 + + + app/components/login/login.component.html + 15 + + Login + + + Logout + Esci + + app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 28 + + Logout + + + Create admin account + Crea un account amministratore + + app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html + 1 + + Create admin account dialog title + + + No default admin account detected. This will create and set the password for an admin account with the user name as 'admin'. + Nessun account amministratore predefinito rilevato. Verrà creato e impostata la password per un account amministratore con il nome utente "admin". + + app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html + 5 + + No default admin detected explanation + + + Create + Crea + + app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html + 17 + + Create + + + Profile + Profilo + + app/app.component.html + 19 + + Profile menu label + + + About + Informazioni + + app/app.component.html + 32 + + About menu label + + + Home + Pagina principale + + app/app.component.html + 43 + + Navigation menu Home Page title + + + Subscriptions + Iscrizioni + + app/app.component.html + 45 + + Navigation menu Subscriptions Page title + + + Downloads + Download + + app/app.component.html + 46 + + Navigation menu Downloads Page title + + + Share playlist + Condividi playlist + + app/dialogs/share-media-dialog/share-media-dialog.component.html + 2 + + Share playlist dialog title + + + Share video + Condividi il video + + app/dialogs/share-media-dialog/share-media-dialog.component.html + 3 + + Share video dialog title + + + Share audio + Condividi l'audio + + app/dialogs/share-media-dialog/share-media-dialog.component.html + 4 + + Share audio dialog title + + + Enable sharing + Abilita la condivisione + + app/dialogs/share-media-dialog/share-media-dialog.component.html + 10 + + Enable sharing checkbox + + + Use timestamp + Usa data e ora + + app/dialogs/share-media-dialog/share-media-dialog.component.html + 13 + + Use timestamp + + + Seconds + Secondi + + app/dialogs/share-media-dialog/share-media-dialog.component.html + 15 + + Seconds + + + Copy to clipboard + Copia negli appunti + + app/dialogs/share-media-dialog/share-media-dialog.component.html + 24 + + Copy to clipboard button + + + Save changes + Salva le modifiche + + app/player/player.component.html + 22 + + Playlist save changes button + + + The download was successful + Il download è riuscito + + app/download-item/download-item.component.html + 8 + + download successful tooltip + + + An error has occurred + Si è verificato un errore + + app/download-item/download-item.component.html + 9 + + download error tooltip + + + Details + Dettagli + + app/download-item/download-item.component.html + 18 + + Details + + + An error has occurred: + Si è verificato un errore: + + app/download-item/download-item.component.html + 27 + + Error label + + + Download start: + Inizio download: + + app/download-item/download-item.component.html + 32 + + Download start label + + + Download end: + Termine download: + + app/download-item/download-item.component.html + 35 + + Download end label + + + File path(s): + Percorso(i) file: + + app/download-item/download-item.component.html + 38 + + File path(s) label + + + Subscribe to playlist or channel + Iscriviti alla playlist o al canale + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 1 + + Subscribe dialog title + + + The playlist or channel URL + URL della playlist o del canale + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 9 + + Subscription URL input hint + + + Custom name + Nome personalizzato + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 19 + + Subscription custom name placeholder + + + Download all uploads + Scarica tutti i file caricati + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 23 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 7 + + Download all uploads subscription setting + + + Download videos uploaded in the last + Scarica i video caricati negli ultimi + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 26 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 10 + + Download time range prefix + + + Audio-only mode + Modalità solo audio + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 40 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 24 + + Streaming-only mode + + + Streaming-only mode + Modalità solo streaming + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 45 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 29 + + Streaming-only mode + + + These are added after the standard args. + Questi vengono aggiunti dopo ai parametri standard. + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 53 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 37 + + Custom args hint + + + Custom file output + File di uscita personalizzato + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 59 + + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 43 + + Subscription custom file output placeholder + + + Subscribe + Iscriviti + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 74 + + Subscribe button + + + Type: + Tipo: + + app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 5 + + Subscription type property + + + Archive: + Archivio: + + app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 17 + + Subscription ID property + + + Export Archive + Esporta archivio + + app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 24 + + Export Archive button + + + Unsubscribe + Annulla l'iscrizione + + app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 26 + + Unsubscribe button + + + Your subscriptions + I tuoi abbonamenti + + app/subscriptions/subscriptions.component.html + 3 + + Subscriptions title + + + Channels + Canali + + app/subscriptions/subscriptions.component.html + 8 + + Subscriptions channels title + + + Name not available. Channel retrieval in progress. + Nome non disponibile. Recupero del canale in corso. + + app/subscriptions/subscriptions.component.html + 14 + + Subscription playlist not available text + + + You have no channel subscriptions. + Non sei iscritto a nessun canale. + + app/subscriptions/subscriptions.component.html + 24 + + No channel subscriptions text + + + Playlists + Scalette + + app/subscriptions/subscriptions.component.html + 27 + + Subscriptions playlists title + + + Name not available. Playlist retrieval in progress. + Nome non disponibile. Recupero playlist in corso. + + app/subscriptions/subscriptions.component.html + 33 + + Subscription playlist not available text + + + You have no playlist subscriptions. + Non sei iscritto a nessuna playlist. + + app/subscriptions/subscriptions.component.html + 43 + + No playlist subscriptions text + + + Editing + Modifica + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 1 + + Edit subscription dialog title prefix + + + Search + Cerca + + app/subscription/subscription/subscription.component.html + 32 + + + app/components/modify-users/modify-users.component.html + 7 + + + app/components/recent-videos/recent-videos.component.html + 24 + + Subscription videos search placeholder + + + Length: + Durata: + + app/subscription/subscription-file-card/subscription-file-card.component.html + 3 + + Video duration label + + + Delete and redownload + Elimina e scarica di nuovo + + app/subscription/subscription-file-card/subscription-file-card.component.html + 8 + + + app/components/unified-file-card/unified-file-card.component.html + 23 + + Delete and redownload subscription video button + + + Delete forever + Elimina definitivamente + + app/subscription/subscription-file-card/subscription-file-card.component.html + 9 + + + app/components/unified-file-card/unified-file-card.component.html + 26 + + Delete forever subscription video button + + + Updater + Aggiornato da + + app/dialogs/update-progress-dialog/update-progress-dialog.component.html + 1 + + Update progress dialog title + + + Select a version: + Seleziona una versione: + + app/updater/updater.component.html + 3 + + Select a version + + + Register + Registrati + + app/components/login/login.component.html + 35 + + + app/dialogs/add-user-dialog/add-user-dialog.component.html + 17 + + Register + + + Session ID: + ID sessione: + + app/components/downloads/downloads.component.html + 5 + + Session ID + + + (current) + (attuale) + + app/components/downloads/downloads.component.html + 6 + + Current session + + + Clear all downloads + Cancella tutti i download + + app/components/downloads/downloads.component.html + 18 + + clear all downloads action button + + + No downloads available! + Nessun download disponibile! + + app/components/downloads/downloads.component.html + 25 + + No downloads label + + + Register a user + Registra un utente + + app/dialogs/add-user-dialog/add-user-dialog.component.html + 1 + + Register user dialog title + + + User name + Nome utente + + app/dialogs/add-user-dialog/add-user-dialog.component.html + 6 + + User name placeholder + + + Manage user + Gestisci utente + + app/components/manage-user/manage-user.component.html + 1 + + + app/components/modify-users/modify-users.component.html + 70 + + Manage user dialog title + + + User UID: + UID utente: + + app/components/manage-user/manage-user.component.html + 4 + + User UID + + + New password + Nuova password + + app/components/manage-user/manage-user.component.html + 8 + + New password placeholder + + + Set new password + Imposta nuova password + + app/components/manage-user/manage-user.component.html + 10 + + Set new password + + + Use role default + Usa il ruolo predefinito + + app/components/manage-user/manage-user.component.html + 19 + + Use role default + + + Yes + + + app/components/manage-user/manage-user.component.html + 20 + + + app/components/manage-role/manage-role.component.html + 9 + + Yes + + + No + No + + app/components/manage-user/manage-user.component.html + 21 + + + app/components/manage-role/manage-role.component.html + 10 + + No + + + Manage role + Gestisci ruolo + + app/components/manage-role/manage-role.component.html + 1 + + Manage role dialog title + + + User name + Nome utente + + app/components/modify-users/modify-users.component.html + 17 + + Username users table header + + + Role + Ruolo + + app/components/modify-users/modify-users.component.html + 35 + + Role users table header + + + Actions + Azioni + + app/components/modify-users/modify-users.component.html + 55 + + Actions users table header + + + Edit user + Modifica utente + + app/components/modify-users/modify-users.component.html + 66 + + edit user action button tooltip + + + Delete user + Elimina utente + + app/components/modify-users/modify-users.component.html + 73 + + delete user action button tooltip + + + Add Users + Aggiungi utenti + + app/components/modify-users/modify-users.component.html + 90 + + Add users button + + + Edit Role + Modifica ruolo + + app/components/modify-users/modify-users.component.html + 95 + + Edit role + + + Lines: + Linee: + + app/components/logs-viewer/logs-viewer.component.html + 22 + + Label for lines select in logger view + + + Clear logs + Cancella i registri + + app/components/logs-viewer/logs-viewer.component.html + 34 + + Clear logs button + + + Open file + Apri il file + + app/components/unified-file-card/unified-file-card.component.html + 13 + + Open file button + + + Open file in new tab + Apri il file in una nuova scheda + + app/components/unified-file-card/unified-file-card.component.html + 14 + + Open file in new tab + + + Go to subscription + Vai alle iscrizioni + + app/components/unified-file-card/unified-file-card.component.html + 20 + + Go to subscription menu item + + + My videos + I miei video + + app/components/recent-videos/recent-videos.component.html + 20 + + My videos title + + + Global custom args + Parametri personalizzati generali + + src/app/settings/settings.component.html + 133 + + Custom args input placeholder + + + See less. + Nascondi. + + src/app/components/see-more/see-more.component.html + 8,9 + + See less + + + Also known as a Client ID. + Conosciuto anche come Client ID. + + src/app/settings/settings.component.html + 249 + + Twitch API Key setting hint AKA preamble + + + Default file output + File output predefinito + + src/app/settings/settings.component.html + 123 + + Default file output placeholder + + + Redownload fresh uploads + Riscarica i nuovi contenuti + + src/app/settings/settings.component.html + 63 + + Redownload fresh uploads + + + Sometimes new videos are downloaded before being fully processed. This setting will mean new videos will be checked for a higher quality version the following day. + A volte i nuovi video vengono scaricati prima di essere completamente elaborati. Questa impostazione significa che per i nuovi video verrà effettuata una verifica il giorno successivo per la ricerca di versioni di qualità superiore. + + src/app/settings/settings.component.html + 63 + + Redownload fresh uploads tooltip + + + See more. + Vedi altro. + + src/app/components/see-more/see-more.component.html + 5,6 + + See more + + + Auto-generated + Generato automaticamente + + src/app/components/unified-file-card/unified-file-card.component.html + 5 + + Auto-generated label + + + Select a download agent + Seleziona un agente di download + + src/app/settings/settings.component.html + 299 + + Custom downloader select label + + + Auto-download Twitch Chat + Scarica automaticamente le Chat Twitch + + src/app/settings/settings.component.html + 244 + + Auto download Twitch Chat setting + + + Twitch API Key + Chiave dell'API Twitch + + src/app/settings/settings.component.html + 248 + + Twitch API Key setting placeholder + + + Use Twitch API + Usa l'API Twitch + + src/app/settings/settings.component.html + 241 + + Use Twitch API setting + + + Categories + Categorie + + src/app/settings/settings.component.html + 144 + + Categories + + + Path is relative to the above download paths. Don't include extension. + Il percorso è riferito ai percorsi di download sopra. Non includere l'estensione. + + src/app/settings/settings.component.html + 126 + + Custom Output input hint + + + views + visualizzazioni + + src/app/player/player.component.html + 15 + + View count label + + + Download Twitch Chat + Scarica Chat Twitch + + src/app/components/twitch-chat/twitch-chat.component.html + 10 + + Download Twitch Chat button + + + Add new rule + Aggiungi nuova regola + + src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html + 39 + + Add new rule tooltip + + + Rules + Regole + + src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html + 10 + + Rules + + + Editing category + Categoria in modifica + + src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html + 1 + + Editing category dialog title + + + Paused + In pausa + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 7 + + Paused subscription setting + + + No videos found. + Nessun video trovato. + + src/app/components/recent-videos/recent-videos.component.html + 38 + + No videos found + + + Reverse order + Ordine inverso + + src/app/dialogs/modify-playlist/modify-playlist.component.html + 14 + + Reverse order + + + Normal order + Ordine normale + + src/app/dialogs/modify-playlist/modify-playlist.component.html + 13 + + Normal order + + + Add content + Aggiungi contenuto + + src/app/dialogs/modify-playlist/modify-playlist.component.html + 19 + + Add content + + + Category: + Categoria: + + src/app/dialogs/video-info-dialog/video-info-dialog.component.html + 29 + + Category property + + + (Paused) + (In pausa) + + src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 1 + + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 1 + + + src/app/subscriptions/subscriptions.component.html + 12 + + + src/app/subscriptions/subscriptions.component.html + 31 + + + src/app/subscription/subscription/subscription.component.html + 5 + + Paused suffix + + + Max quality + Qualità massima + + src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 40 + + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 32 + + Max quality placeholder + + + + diff --git a/src/assets/i18n/messages.zh.json b/src/assets/i18n/messages.zh.json index ab8be32..683f6b9 100644 --- a/src/assets/i18n/messages.zh.json +++ b/src/assets/i18n/messages.zh.json @@ -113,7 +113,7 @@ "61b81b11aad0b9d970ece2fce18405f07eac69c2": "只需将下面的链接拖放到书签栏中。在YouTube页面上您只需单击书签即可下载视频。", "c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "生成“仅音频”书签", "d5f69691f9f05711633128b5a3db696783266b58": "额外", - "5fab47f146b0a4b809dcebf3db9da94df6299ea1": "使用默认下载代理", + "5fab47f146b0a4b809dcebf3db9da94df6299ea1": "使用默认下载程序", "ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "选择下载器", "00e274c496b094a019f0679c3fab3945793f3335": "选择日志级别", "dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "开启高级下载选项", @@ -124,7 +124,7 @@ "4d13a9cd5ed3dcee0eab22cb25198d43886942be": "用户", "eb3d5aefff38a814b76da74371cbf02c0789a1ef": "日志", "52c9a103b812f258bcddc3d90a6e3f46871d25fe": "保存", - "fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {关} false {取消} }", + "fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {关} false {取消} other {其他} }", "cec82c0a545f37420d55a9b6c45c20546e82f94e": "关于 YoutubeDL-Material", "199c17e5d6a419313af3c325f06dcbb9645ca618": "是根据Google的Material Design规范构建的开源YouTube下载器。您可以将喜欢的视频下载为视频或音频文件,并且可以订阅喜欢的频道和播放列表,以便及时下载他们的新视频。", "bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "包含很多很棒的功能!支持API,Docker和本地化。在Github上查找所有受支持的功能。", @@ -238,5 +238,32 @@ "cfa67d14d84fe0e9fadf251dc51ffc181173b662": "搜索起点", "544e09cdc99a8978f48521d45f62db0da6dcf742": "使用角色预设", "3697f8583ea42868aa269489ad366103d94aece7": "编辑中", - "fb35145bfb84521e21b6385363d59221f436a573": "取消所有下载" + "fb35145bfb84521e21b6385363d59221f436a573": "取消所有下载", + "56a2a773fbd5a6b9ac2e6b89d29d70a2ed0f3227": "查看更少", + "c776eb4992b6c98f58cd89b20c1ea8ac37888521": "选择一个下载程序", + "d641b8fa5ac5e85114c733b1f7de6976bd091f70": "最高画质", + "ddc31f2885b1b33a7651963254b0c197f2a64086": "查看更多...", + "5fb1e0083c9b2a40ac8ae7dcb2618311c291b8b9": "自动下载Twitch弹幕", + "84ffcebac2709ca0785f4a1d5ba274433b5beabc": "也称为客户ID", + "8ae23bc4302a479f687f4b20a84c276182e2519c": "Twitch API 密钥", + "d162f9fcd6a7187b391e004f072ab3da8377c47d": "使用Twitch API", + "04201f9d27abd7d6f58a4328ab98063ce1072006": "分类", + "ef418d4ece7c844f3a5e431da1aa59bedd88da7b": "全局自定义变量", + "1148fd45287ff09955b938756bc302042bcb29c7": "路径相对于上述下载路径,不包括扩展名。", + "cfe829634b1144bc44b6d38cf5584ea65db9804f": "默认输出文件夹", + "3d1a47dc18b7bd8b5d9e1eb44b235ed9c4a2b513": "重新下载新上传的内容", + "13759b09a7f4074ceee8fa2f968f9815fdf63295": "有时新视频会在完全处理前下载。这项设置指新视频会在第二天检查视频是否有更高画质。", + "dad95154dcef3509b8cc705046061fd24994bbb7": "浏览", + "792dc6a57f28a1066db283f2e736484f066005fd": "下载Twitch弹幕", + "e4eeb9106dbcbc91ca1ac3fb4068915998a70f37": "添加新规则", + "2489eefea00931942b91f4a1ae109514b591e2e1": "规则", + "c3b0b86523f1d10e84a71f9b188d54913a11af3b": "编辑类别", + "07db550ae114d9faad3a0cbb68bcc16ab6cd31fc": "暂停", + "73423607944a694ce6f9e55cfee329681bb4d9f9": "找不到视频", + "29376982b1205d9d6ea3d289e8e2f8e1ac2839b1": "倒序", + "33026f57ea65cd9c8a5d917a08083f71a718933a": "正序", + "5caadefa4143cf6766a621b0f54f91f373a1f164": "添加内容", + "0cc1dec590ecd74bef71a865fb364779bc42a749": "类别:", + "303e45ffae995c9817e510e38cb969e6bb3adcbf": "(暂停)", + "24dc3ecf7ec2c2144910c4f3d38343828be03a4c": "自动生成的" } \ No newline at end of file diff --git a/src/assets/i18n/messages.zh.xlf b/src/assets/i18n/messages.zh.xlf new file mode 100644 index 0000000..a0b6008 --- /dev/null +++ b/src/assets/i18n/messages.zh.xlf @@ -0,0 +1,2578 @@ + + + + + + Create a playlist + 创建播放列表 + + app/create-playlist/create-playlist.component.html + 1 + + Create a playlist dialog title + + + Name + 名称 + + app/create-playlist/create-playlist.component.html + 5 + + + app/dialogs/modify-playlist/modify-playlist.component.html + 7 + + Playlist name placeholder + + + Audio files + 音频文件 + + app/create-playlist/create-playlist.component.html + 10 + + Audio files title + + + Videos + 视频文件 + + app/create-playlist/create-playlist.component.html + 11 + + + app/subscription/subscription/subscription.component.html + 28 + + Videos title + + + Modify youtube-dl args + 修改youtube-dl参数 + + app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 1 + + Modify args title + + + Simulated new args + 模拟新参数 + + app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 8 + + Simulated args title + + + Add an arg + 添加参数 + + app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 34 + + Add arg card title + + + Search by category + 按类别搜索 + + app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 60 + + Search args by category button + + + Use arg value + 使用参数值 + + app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 64 + + Use arg value checkbox + + + Arg value + 参数值 + + app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 68 + + Arg value placeholder + + + Add arg + 添加参数 + + app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 73 + + Search args by category button + + + Cancel + 取消 + + app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 84 + + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 70 + + Arg modifier cancel button + + + Modify + 修改 + + app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 85 + + Arg modifier modify button + + + Youtube Downloader + Youtube下载器 + + app/main/main.component.html + 5 + + Youtube downloader home page label + + + Quality + 质量 + + app/main/main.component.html + 21 + + Quality select label + + + Use URL + 使用URL + + app/main/main.component.html + 49 + + YT search Use URL button for searched video + + + View + 查看 + + app/main/main.component.html + 52 + + YT search View button for searched video + + + Only Audio + 仅音频 + + app/main/main.component.html + 62 + + Only Audio checkbox + + + Multi-download Mode + 多下载模式 + + app/main/main.component.html + 67 + + Multi-download Mode checkbox + + + Download + 下载 + + app/main/main.component.html + 76 + + Main download button + + + Cancel + 取消 + + app/main/main.component.html + 81 + + Cancel download button + + + Advanced + 高级 + + app/main/main.component.html + 93 + + Advanced download mode panel + + + Simulated command: + 模拟命令: + + app/main/main.component.html + 99 + + Simulated command label + + + Use custom args + 使用自定义参数 + + app/main/main.component.html + 107 + + Use custom args checkbox + + + Custom args + 自定义参数 + + app/main/main.component.html + 113 + + + app/settings/settings.component.html + 145 + + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 48 + + Custom args placeholder + + + No need to include URL, just everything after. Args are delimited using two commas like so: ,, + 不必指定URL,仅需指定其后的部分。参数用两个逗号分隔:,, + + app/main/main.component.html + 115 + + Custom Args input hint + + + Use custom output + 使用自定义输出 + + app/main/main.component.html + 123 + + Use custom output checkbox + + + Custom output + 自定义输出 + + app/main/main.component.html + 128 + + Custom output placeholder + + + Documentation + 文档 + + app/main/main.component.html + 130 + + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 60 + + Youtube-dl output template documentation link + + + Path is relative to the config download path. Don't include extension. + 该路径是相对于配置下载路径的,省略文件扩展名 + + app/main/main.component.html + 131 + + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 61 + + Custom Output input hint + + + Use authentication + 使用身份验证 + + app/main/main.component.html + 137 + + Use authentication checkbox + + + Username + 用户名 + + app/main/main.component.html + 142 + + YT Username placeholder + + + Password + 密码 + + app/main/main.component.html + 147 + + + app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html + 10 + + + app/dialogs/add-user-dialog/add-user-dialog.component.html + 11 + + YT Password placeholder + + + Audio + 音频 + + app/main/main.component.html + 191 + + Audio files title + + + Your audio files are here + 您的音频文件在这里 + + app/main/main.component.html + 196 + + Audio files description + + + Playlists + 播放列表 + + app/main/main.component.html + 211 + + + app/main/main.component.html + 253 + + + app/subscriptions/subscriptions.component.html + 27 + + Playlists title + + + No playlists available. Create one from your downloading audio files by clicking the blue plus button. + 没有可用的播放列表。 通过单击蓝色加号按钮从您下载的音频文件创建一个。 + + app/main/main.component.html + 222 + + No video playlists available text + + + Video + 视频 + + app/main/main.component.html + 232 + + Video files title + + + Your video files are here + 您的视频文件在这里 + + app/main/main.component.html + 237 + + Video files description + + + No playlists available. Create one from your downloading video files by clicking the blue plus button. + 没有可用的播放列表。 通过单击蓝色加号按钮,从下载的视频文件中创建一个。 + + app/main/main.component.html + 266 + + No video playlists available text + + + Name: + 名称: + + app/dialogs/video-info-dialog/video-info-dialog.component.html + 5 + + + app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 6 + + Video name property + + + URL: + URL: + + app/dialogs/video-info-dialog/video-info-dialog.component.html + 9 + + + app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 9 + + Video URL property + + + Uploader: + 上传者: + + app/dialogs/video-info-dialog/video-info-dialog.component.html + 13 + + Video ID property + + + File size: + 文件大小: + + app/dialogs/video-info-dialog/video-info-dialog.component.html + 17 + + Video file size property + + + Path: + 路径: + + app/dialogs/video-info-dialog/video-info-dialog.component.html + 21 + + Video path property + + + Upload Date: + 上传日期: + + app/dialogs/video-info-dialog/video-info-dialog.component.html + 25 + + Video upload date property + + + Close + 关闭 + + app/dialogs/video-info-dialog/video-info-dialog.component.html + 31 + + + app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html + 40 + + + app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 27 + + + app/dialogs/share-media-dialog/share-media-dialog.component.html + 30 + + + app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 23 + + + app/dialogs/update-progress-dialog/update-progress-dialog.component.html + 17 + + + app/dialogs/add-user-dialog/add-user-dialog.component.html + 18 + + + app/components/manage-user/manage-user.component.html + 30 + + + app/components/manage-role/manage-role.component.html + 18 + + Close subscription info button + + + Modify playlist + 修改播放列表 + + app/dialogs/modify-playlist/modify-playlist.component.html + 1 + + Modify playlist dialog title + + + ID: + ID: + + app/file-card/file-card.component.html + 7 + + + app/download-item/download-item.component.html + 4 + + + app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 13 + + File or playlist ID + + + Count: + 数量: + + app/file-card/file-card.component.html + 8 + + Playlist video count + + + Edit + 编辑 + + app/file-card/file-card.component.html + 19 + + Playlist edit button + + + Delete + 删除 + + app/file-card/file-card.component.html + 20 + + + app/file-card/file-card.component.html + 25 + + Delete playlist + + + Info + 详情 + + app/file-card/file-card.component.html + 24 + + + app/subscription/subscription-file-card/subscription-file-card.component.html + 7 + + Video info button + + + Delete and blacklist + 删除并拉黑 + + app/file-card/file-card.component.html + 26 + + Delete and blacklist video button + + + Upload new cookies + 上传新Cookies + + app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html + 1 + + Cookies uploader dialog title + + + Drag and Drop + 拖放 + + app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html + 11 + + Drag and Drop + + + NOTE: Uploading new cookies will overrride your previous cookies. Also note that cookies are instance-wide, not per-user. + 注意:加载新的Cookies将覆盖您以前的Cookie。并且Cookies的范围是整个实例,而不是每个用户单独分开的。 + + app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html + 20 + + Cookies upload warning + + + Settings + 设置 + + app/settings/settings.component.html + 1 + + + app/app.component.html + 28 + + Settings title + + + URL + URL + + app/settings/settings.component.html + 18 + + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 8 + + URL input placeholder + + + URL this app will be accessed from, without the port. + 设置访问URL,无需端口。 + + app/settings/settings.component.html + 19 + + URL setting input hint + + + Port + 端口 + + app/settings/settings.component.html + 24 + + Port input placeholder + + + The desired port. Default is 17442. + 设置目标端口。默认为17442。 + + app/settings/settings.component.html + 25 + + Port setting input hint + + + Multi-user mode + 多用户模式 + + app/settings/settings.component.html + 34 + + Multi user mode setting + + + Users base path + 用户文件路径 + + app/settings/settings.component.html + 38 + + Users base path placeholder + + + Base path for users and their downloaded videos. + 用户及其下载视频的文件路径。 + + app/settings/settings.component.html + 39 + + Users base path hint + + + Use encryption + 使用加密(SSL) + + app/settings/settings.component.html + 48 + + Use encryption setting + + + Cert file path + 证书文件路径 + + app/settings/settings.component.html + 53 + + Cert file path input placeholder + + + Key file path + 密钥文件路径 + + app/settings/settings.component.html + 59 + + Key file path input placeholder + + + Allow subscriptions + 允许订阅 + + app/settings/settings.component.html + 68 + + Allow subscriptions setting + + + Subscriptions base path + 订阅文件路径 + + app/settings/settings.component.html + 72 + + Subscriptions base path input setting placeholder + + + Base path for videos from your subscribed channels and playlists. It is relative to YTDL-Material's root folder. + 订阅频道和播放列表中视频的文件路径(相对于根文件夹而言)。 + + app/settings/settings.component.html + 73 + + Subscriptions base path setting input hint + + + Check interval + 检查间隔 + + app/settings/settings.component.html + 78 + + Check interval input setting placeholder + + + Unit is seconds, only include numbers. + 单位是秒,只包含数字。 + + app/settings/settings.component.html + 79 + + Check interval setting input hint + + + Use youtube-dl archive + 使用youtube-dl存档 + + app/settings/settings.component.html + 83 + + + app/settings/settings.component.html + 152 + + Use youtube-dl archive setting + + + With youtube-dl's archive + 根据youtube-dl的存档功能 + + app/settings/settings.component.html + 84 + + youtube-dl archive explanation prefix link + + + feature, downloaded videos from your subscriptions get recorded in a text file in the subscriptions archive sub-directory. + 从您的订阅下载的视频会记录在订阅存档子目录中的文本文件中。 + + app/settings/settings.component.html + 84 + + youtube-dl archive explanation middle + + + This enables the ability to permanently delete videos from your subscriptions without unsubscribing, and allows you to record which videos you downloaded in case of data loss. + 这样一来,您无需取消订阅便可以从订阅中永久删除视频。并且它还可以在数据丢失的情况下记录已经下载了哪些视频。 + + app/settings/settings.component.html + 85 + + youtube-dl archive explanation suffix + + + Theme + 主题 + + app/settings/settings.component.html + 94 + + Theme select label + + + Default + 默认 + + app/settings/settings.component.html + 96 + + Default theme label + + + Dark + 暗黑 + + app/settings/settings.component.html + 97 + + + app/app.component.html + 23 + + Dark theme label + + + Allow theme change + 允许更改主题 + + app/settings/settings.component.html + 102 + + Allow theme change setting + + + Language + 语言 + + app/settings/settings.component.html + 111 + + Language select label + + + Main + 常规 + + app/settings/settings.component.html + 12 + + Main settings label + + + Audio folder path + 音频文件夹路径 + + app/settings/settings.component.html + 131 + + Audio folder path input placeholder + + + Path for audio only downloads. It is relative to YTDL-Material's root folder. + 音频下载的文件路径。相对于YTDL-Material的根文件夹。 + + app/settings/settings.component.html + 132 + + Aduio path setting input hint + + + Video folder path + 视频文件夹路径 + + app/settings/settings.component.html + 138 + + Video folder path input placeholder + + + Path for video downloads. It is relative to YTDL-Material's root folder. + 视频下载的文件路径。相对于YTDL-Material的根文件夹。 + + app/settings/settings.component.html + 139 + + Video path setting input hint + + + Global custom args for downloads on the home page. Args are delimited using two commas like so: ,, + 开始页面上用于下载的全局自定义参数。参数由两个逗号分隔:,, + + app/settings/settings.component.html + 146 + + Custom args setting input hint + + + Safe download override + 安全下载覆盖 + + app/settings/settings.component.html + 157 + + Safe download override setting + + + Downloader + 下载程序 + + app/settings/settings.component.html + 124 + + Downloader settings label + + + Top title + 首页标题 + + app/settings/settings.component.html + 170 + + Top title input placeholder + + + File manager enabled + 启用文件管理 + + app/settings/settings.component.html + 175 + + File manager enabled setting + + + Downloads manager enabled + 启用下载管理 + + app/settings/settings.component.html + 178 + + Downloads manager enabled setting + + + Allow quality select + 允许选择下载质量 + + app/settings/settings.component.html + 181 + + Allow quality seelct setting + + + Download only mode + 仅下载模式 + + app/settings/settings.component.html + 184 + + Download only mode setting + + + Allow multi-download mode + 开启多下载模式 + + app/settings/settings.component.html + 187 + + Allow multi-download mode setting + + + Require pin for settings + 使用PIN码保护设置 + + app/settings/settings.component.html + 190 + + Require pin for settings setting + + + Set New Pin + 设置新PIN码 + + app/settings/settings.component.html + 191 + + Set new pin button + + + Enable Public API + 启用公共API + + app/settings/settings.component.html + 199 + + Enable Public API key setting + + + Public API Key + 公共API密钥 + + app/settings/settings.component.html + 204 + + Public API Key setting placeholder + + + View documentation + 查看文档 + + app/settings/settings.component.html + 205 + + View API docs setting hint + + + Generate + 生成 + + app/settings/settings.component.html + 209 + + Generate key button + + + Use YouTube API + 使用YouTube API + + app/settings/settings.component.html + 218 + + Use YouTube API setting + + + Youtube API Key + Youtube API密钥 + + app/settings/settings.component.html + 222 + + Youtube API Key setting placeholder + + + Generating a key is easy! + 生成密钥很简单! + + app/settings/settings.component.html + 223 + + Youtube API Key setting hint + + + Click here + 点击这里 + + app/settings/settings.component.html + 233 + + + app/settings/settings.component.html + 239 + + + app/dialogs/about-dialog/about-dialog.component.html + 25 + + Chrome ext click here + + + to download the official YoutubeDL-Material Chrome extension manually. + 来手动下载官方的YoutubeDL-Material Chrome扩展程序。 + + app/settings/settings.component.html + 233 + + Chrome click here suffix + + + You must manually load the extension and modify the extension's settings to set the frontend URL. + 您必须手动安装扩展,并且在扩展的设置中输入下载器URL。 + + app/settings/settings.component.html + 234 + + Chrome setup suffix + + + to install the official YoutubeDL-Material Firefox extension right off the Firefox extensions page. + 直接从Firefox扩展商店安装官方的YoutubeDL-Material Firefox扩展程序。 + + app/settings/settings.component.html + 239 + + Firefox click here suffix + + + Detailed setup instructions. + 详细的扩展说明。 + + app/settings/settings.component.html + 240 + + Firefox setup prefix link + + + Not much is required other than changing the extension's settings to set the frontend URL. + 只需在扩展的设置中输入前端URL。 + + app/settings/settings.component.html + 240 + + Firefox setup suffix + + + Drag the link below to your bookmarks, and you're good to go! Just navigate to the YouTube video you'd like to download, and click the bookmark. + 只需将下面的链接拖放到书签栏中。在YouTube页面上您只需单击书签即可下载视频。 + + app/settings/settings.component.html + 245 + + Bookmarklet instructions + + + Generate 'audio only' bookmarklet + 生成“仅音频”书签 + + app/settings/settings.component.html + 246 + + Generate audio only bookmarklet checkbox + + + Extra + 额外 + + app/settings/settings.component.html + 164 + + Extra settings label + + + Use default downloading agent + 使用默认下载程序 + + app/settings/settings.component.html + 260 + + Use default downloading agent setting + + + Select a downloader + 选择下载器 + + app/settings/settings.component.html + 264 + + Custom downloader select label + + + Select a logger level + 选择日志级别 + + app/settings/settings.component.html + 278 + + Logger level select label + + + Allow advanced download + 开启高级下载选项 + + app/settings/settings.component.html + 289 + + Allow advanced downloading setting + + + Use Cookies + 使用Cookies + + app/settings/settings.component.html + 297 + + Use cookies setting + + + Set Cookies + 设置Cookies + + app/settings/settings.component.html + 298 + + Set cookies button + + + Advanced + 高级 + + app/settings/settings.component.html + 255 + + Host settings label + + + Allow user registration + 允许用户注册 + + app/settings/settings.component.html + 310 + + Allow registration setting + + + Users + 用户 + + app/settings/settings.component.html + 308 + + Users settings label + + + Logs + 日志 + + app/settings/settings.component.html + 314 + + Logs settings label + + + Save + 保存 + + app/settings/settings.component.html + 327 + + Settings save button + + + {VAR_SELECT, select, true {Close} false {Cancel} other {otha}} + {VAR_SELECT, select, true {关} false {取消} other {其他} } + + app/settings/settings.component.html + 330 + + Settings cancel and close button + + + About YoutubeDL-Material + 关于 YoutubeDL-Material + + app/dialogs/about-dialog/about-dialog.component.html + 1 + + About dialog title + + + is an open-source YouTube downloader built under Google's Material Design specifications. You can seamlessly download your favorite videos as video or audio files, and even subscribe to your favorite channels and playlists to keep updated with their new videos. + 是根据Google的Material Design规范构建的开源YouTube下载器。您可以将喜欢的视频下载为视频或音频文件,并且可以订阅喜欢的频道和播放列表,以便及时下载他们的新视频。 + + app/dialogs/about-dialog/about-dialog.component.html + 12 + + About first paragraph + + + has some awesome features included! An extensive API, Docker support, and localization (translation) support. Read up on all the supported features by clicking on the GitHub icon above. + 包含很多很棒的功能!支持API,Docker和本地化。在Github上查找所有受支持的功能。 + + app/dialogs/about-dialog/about-dialog.component.html + 15 + + About second paragraph + + + Installed version: + 安装版本: + + app/dialogs/about-dialog/about-dialog.component.html + 20 + + Version label + + + Checking for updates... + 检查更新... + + app/dialogs/about-dialog/about-dialog.component.html + 20 + + Checking for updates text + + + Update available + 更新可用 + + app/dialogs/about-dialog/about-dialog.component.html + 21 + + View latest update + + + You can update from the settings menu. + 您可以从设置菜单进行更新。 + + app/dialogs/about-dialog/about-dialog.component.html + 21 + + Update through settings menu hint + + + Found a bug or have a suggestion? + 发现了一个错误或有一些建议? + + app/dialogs/about-dialog/about-dialog.component.html + 25 + + About bug prefix + + + to create an issue! + 创建新issue! + + app/dialogs/about-dialog/about-dialog.component.html + 25 + + About bug suffix + + + Your Profile + 您的个人资料 + + app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 1 + + User profile dialog title + + + UID: + UID: + + app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 9 + + UID + + + Created: + 创建日期: + + app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 12 + + Created + + + You are not logged in. + 您尚未登录。 + + app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 19 + + Not logged in notification + + + Login + 登录 + + app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 20 + + + app/app.component.html + 44 + + + app/components/login/login.component.html + 15 + + Login + + + Logout + 注销 + + app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 28 + + Logout + + + Create admin account + 创建管理员帐户 + + app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html + 1 + + Create admin account dialog title + + + No default admin account detected. This will create and set the password for an admin account with the user name as 'admin'. + 未检测到默认管理员帐户。即将创建一个名为admin的管理员帐户并设置密码。 + + app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html + 5 + + No default admin detected explanation + + + Create + 创建 + + app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html + 17 + + Create + + + Profile + 个人资料 + + app/app.component.html + 19 + + Profile menu label + + + About + 关于 + + app/app.component.html + 32 + + About menu label + + + Home + 首 页 + + app/app.component.html + 43 + + Navigation menu Home Page title + + + Subscriptions + 订 阅 + + app/app.component.html + 45 + + Navigation menu Subscriptions Page title + + + Downloads + 下 载 + + app/app.component.html + 46 + + Navigation menu Downloads Page title + + + Share playlist + 分享播放列表 + + app/dialogs/share-media-dialog/share-media-dialog.component.html + 2 + + Share playlist dialog title + + + Share video + 分享视频 + + app/dialogs/share-media-dialog/share-media-dialog.component.html + 3 + + Share video dialog title + + + Share audio + 分享音频 + + app/dialogs/share-media-dialog/share-media-dialog.component.html + 4 + + Share audio dialog title + + + Enable sharing + 启用共享 + + app/dialogs/share-media-dialog/share-media-dialog.component.html + 10 + + Enable sharing checkbox + + + Use timestamp + 使用时间戳 + + app/dialogs/share-media-dialog/share-media-dialog.component.html + 13 + + Use timestamp + + + Seconds + + + app/dialogs/share-media-dialog/share-media-dialog.component.html + 15 + + Seconds + + + Copy to clipboard + 复制到剪贴板 + + app/dialogs/share-media-dialog/share-media-dialog.component.html + 24 + + + app/components/logs-viewer/logs-viewer.component.html + 7 + + Copy to clipboard button + + + Save changes + 保存更改 + + app/player/player.component.html + 22 + + Playlist save changes button + + + Details + 详细 + + app/download-item/download-item.component.html + 18 + + Details + + + An error has occurred: + 发生错误: + + app/download-item/download-item.component.html + 27 + + Error label + + + Download start: + 下载开始: + + app/download-item/download-item.component.html + 32 + + Download start label + + + Download end: + 下载结束: + + app/download-item/download-item.component.html + 35 + + Download end label + + + File path(s): + 文件路径: + + app/download-item/download-item.component.html + 38 + + File path(s) label + + + Subscribe to playlist or channel + 订阅播放列表或频道 + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 1 + + Subscribe dialog title + + + The playlist or channel URL + 播放列表或频道URL + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 9 + + Subscription URL input hint + + + Custom name + 自定义名称 + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 19 + + Subscription custom name placeholder + + + Download all uploads + 下载所有音视频 + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 23 + + Download all uploads subscription setting + + + Download videos uploaded in the last + 下载最近多久的视频 + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 26 + + Download time range prefix + + + Audio-only mode + 仅音频模式 + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 38 + + Streaming-only mode + + + Streaming-only mode + 仅视频模式 + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 43 + + Streaming-only mode + + + These are added after the standard args. + 这些是在标准参数之后添加的。 + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 51 + + Custom args hint + + + Custom file output + 自定义文件输出 + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 57 + + Subscription custom file output placeholder + + + Subscribe + 订阅 + + app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 72 + + Subscribe button + + + Type: + 类型: + + app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 5 + + Subscription type property + + + Archive: + 存档: + + app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 17 + + Subscription ID property + + + Export Archive + 导出存档 + + app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 24 + + Export Archive button + + + Unsubscribe + 取消订阅 + + app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 26 + + Unsubscribe button + + + Your subscriptions + 您的订阅 + + app/subscriptions/subscriptions.component.html + 3 + + Subscriptions title + + + Channels + 频道 + + app/subscriptions/subscriptions.component.html + 8 + + Subscriptions channels title + + + Name not available. Channel retrieval in progress. + 名称不可用。正在检索频道... + + app/subscriptions/subscriptions.component.html + 14 + + Subscription playlist not available text + + + You have no channel subscriptions. + 您尚未订阅任何频道。 + + app/subscriptions/subscriptions.component.html + 24 + + No channel subscriptions text + + + Name not available. Playlist retrieval in progress. + 名称不可用。正在检索播放列表... + + app/subscriptions/subscriptions.component.html + 33 + + Subscription playlist not available text + + + You have no playlist subscriptions. + 您尚未订阅任何播放列表。 + + app/subscriptions/subscriptions.component.html + 43 + + No playlist subscriptions text + + + Search + 搜索 + + app/subscription/subscription/subscription.component.html + 32 + + Subscription videos search placeholder + + + Length: + 长度: + + app/subscription/subscription-file-card/subscription-file-card.component.html + 3 + + Video duration label + + + Delete and redownload + 删除并重新下载 + + app/subscription/subscription-file-card/subscription-file-card.component.html + 8 + + Delete and redownload subscription video button + + + Delete forever + 永久删除 + + app/subscription/subscription-file-card/subscription-file-card.component.html + 9 + + Delete forever subscription video button + + + Updater + 更新程序 + + app/dialogs/update-progress-dialog/update-progress-dialog.component.html + 1 + + Update progress dialog title + + + Select a version: + 选择版本: + + app/updater/updater.component.html + 3 + + Select a version + + + Register + 注册 + + app/components/login/login.component.html + 35 + + + app/dialogs/add-user-dialog/add-user-dialog.component.html + 17 + + Register + + + Session ID: + 会话ID: + + app/components/downloads/downloads.component.html + 5 + + Session ID + + + (current) + (当前) + + app/components/downloads/downloads.component.html + 6 + + Current session + + + No downloads available! + 没有下载可用! + + app/components/downloads/downloads.component.html + 25 + + No downloads label + + + Register a user + 注册用户 + + app/dialogs/add-user-dialog/add-user-dialog.component.html + 1 + + Register user dialog title + + + User name + 用户名 + + app/dialogs/add-user-dialog/add-user-dialog.component.html + 6 + + User name placeholder + + + Manage user + 管理用户 + + app/components/manage-user/manage-user.component.html + 1 + + Manage user dialog title + + + User UID: + 用户UID: + + app/components/manage-user/manage-user.component.html + 4 + + User UID + + + New password + 新密码 + + app/components/manage-user/manage-user.component.html + 8 + + New password placeholder + + + Set new password + 设置新密码 + + app/components/manage-user/manage-user.component.html + 10 + + Set new password + + + Use default + 使用默认值 + + app/components/manage-user/manage-user.component.html + 19 + + Use default + + + Yes + + + app/components/manage-user/manage-user.component.html + 20 + + + app/components/manage-role/manage-role.component.html + 9 + + Yes + + + No + + + app/components/manage-user/manage-user.component.html + 21 + + + app/components/manage-role/manage-role.component.html + 10 + + No + + + Manage role + 管理用户 + + app/components/manage-role/manage-role.component.html + 1 + + Manage role dialog title + + + User name + 用户名 + + app/components/modify-users/modify-users.component.html + 17 + + Username users table header + + + Role + 身份 + + app/components/modify-users/modify-users.component.html + 35 + + Role users table header + + + Actions + 动作 + + app/components/modify-users/modify-users.component.html + 55 + + Actions users table header + + + Add Users + 添加用户 + + app/components/modify-users/modify-users.component.html + 90 + + Add users button + + + Edit Role + 编辑用户 + + app/components/modify-users/modify-users.component.html + 95 + + Edit role + + + Logs will appear here + 日志将出现在这里 + + app/components/logs-viewer/logs-viewer.component.html + 5 + + Logs placeholder + + + Lines: + 行: + + app/components/logs-viewer/logs-viewer.component.html + 9 + + Label for lines select in logger view + + + Include thumbnail + 包括缩略图 + + app/settings/settings.component.html + 131 + + Include thumbnail setting + + + Type + 类型 + + app/create-playlist/create-playlist.component.html + 11 + + Type select + + + Video + 视频 + + app/create-playlist/create-playlist.component.html + 13 + + Video + + + Audio + 音频 + + app/create-playlist/create-playlist.component.html + 12 + + Audio + + + Open file + 打开文件 + + app/components/unified-file-card/unified-file-card.component.html + 13 + + Open file button + + + Clear logs + 清空日志 + + app/components/logs-viewer/logs-viewer.component.html + 34 + + Clear logs button + + + Delete user + 删除用户 + + app/components/modify-users/modify-users.component.html + 73 + + delete user action button tooltip + + + Edit user + 编辑用户 + + app/components/modify-users/modify-users.component.html + 66 + + edit user action button tooltip + + + Clear all downloads + 清空所有下载 + + app/components/downloads/downloads.component.html + 18 + + clear all downloads action button + + + Bind Credentials + 绑定凭证 + + app/settings/settings.component.html + 328 + + Bind Credentials + + + Bind DN + 绑定DN + + app/settings/settings.component.html + 323 + + Bind DN + + + LDAP URL + LDAP链接 + + app/settings/settings.component.html + 318 + + LDAP URL + + + Auth method + 认证方式 + + app/settings/settings.component.html + 306 + + Auth method select + + + LDAP + LDAP认证 + + app/settings/settings.component.html + 311 + + LDAP auth method + + + Internal + 内部身份验证 + + app/settings/settings.component.html + 308 + + Internal auth method + + + Login expiration + 登录到期 + + app/settings/settings.component.html + 268 + + Login expiration select label + + + Log Level + 日志等级 + + app/settings/settings.component.html + 256 + + Log Level label + + + This will delete your old API key! + 这将删除您的旧API密钥! + + app/settings/settings.component.html + 187 + + delete api key tooltip + + + Include metadata + 包含元数据 + + app/settings/settings.component.html + 135 + + Include metadata setting + + + NOTE: Uploading new cookies will override your previous cookies. Also note that cookies are instance-wide, not per-user. + 注意:加载新的Cookies将覆盖您以前的Cookie。并且Cookies的范围是整个实例,而不是每个用户单独分开的。 + + app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html + 20 + + Cookies upload warning + + + Add more content + 添加更多内容 + + app/dialogs/modify-playlist/modify-playlist.component.html + 17 + + Add more content + + + My videos + 我的视频 + + app/components/recent-videos/recent-videos.component.html + 20 + + My videos title + + + Go to subscription + 前往订阅 + + app/components/unified-file-card/unified-file-card.component.html + 20 + + Go to subscription menu item + + + Open file in new tab + 在新标签页打开文件 + + app/components/unified-file-card/unified-file-card.component.html + 14 + + Open file in new tab + + + An error has occurred + 出现错误 + + app/download-item/download-item.component.html + 9 + + download error tooltip + + + The download was successful + 下载成功 + + app/download-item/download-item.component.html + 8 + + download successful tooltip + + + Search Filter + 搜索过滤器 + + app/settings/settings.component.html + 338 + + Search Filter + + + Search Base + 搜索起点 + + app/settings/settings.component.html + 333 + + Search Base + + + Use role default + 使用角色预设 + + app/components/manage-user/manage-user.component.html + 19 + + Use role default + + + Editing + 编辑中 + + app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 1 + + Edit subscription dialog title prefix + + + Kill all downloads + 取消所有下载 + + app/settings/settings.component.html + 139 + + Kill all downloads button + + + See less. + 查看更少 + + src/app/components/see-more/see-more.component.html + 8,9 + + See less + + + Select a download agent + 选择一个下载程序 + + src/app/settings/settings.component.html + 299 + + Custom downloader select label + + + Max quality + 最高画质 + + src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 40 + + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 32 + + Max quality placeholder + + + See more. + 查看更多... + + src/app/components/see-more/see-more.component.html + 5,6 + + See more + + + Auto-download Twitch Chat + 自动下载Twitch弹幕 + + src/app/settings/settings.component.html + 244 + + Auto download Twitch Chat setting + + + Also known as a Client ID. + 也称为客户ID + + src/app/settings/settings.component.html + 249 + + Twitch API Key setting hint AKA preamble + + + Twitch API Key + Twitch API 密钥 + + src/app/settings/settings.component.html + 248 + + Twitch API Key setting placeholder + + + Use Twitch API + 使用Twitch API + + src/app/settings/settings.component.html + 241 + + Use Twitch API setting + + + Categories + 分类 + + src/app/settings/settings.component.html + 144 + + Categories + + + Global custom args + 全局自定义变量 + + src/app/settings/settings.component.html + 133 + + Custom args input placeholder + + + Path is relative to the above download paths. Don't include extension. + 路径相对于上述下载路径,不包括扩展名。 + + src/app/settings/settings.component.html + 126 + + Custom Output input hint + + + Default file output + 默认输出文件夹 + + src/app/settings/settings.component.html + 123 + + Default file output placeholder + + + Redownload fresh uploads + 重新下载新上传的内容 + + src/app/settings/settings.component.html + 63 + + Redownload fresh uploads + + + Sometimes new videos are downloaded before being fully processed. This setting will mean new videos will be checked for a higher quality version the following day. + 有时新视频会在完全处理前下载。这项设置指新视频会在第二天检查视频是否有更高画质。 + + src/app/settings/settings.component.html + 63 + + Redownload fresh uploads tooltip + + + views + 浏览 + + src/app/player/player.component.html + 15 + + View count label + + + Download Twitch Chat + 下载Twitch弹幕 + + src/app/components/twitch-chat/twitch-chat.component.html + 10 + + Download Twitch Chat button + + + Add new rule + 添加新规则 + + src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html + 39 + + Add new rule tooltip + + + Rules + 规则 + + src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html + 10 + + Rules + + + Editing category + 编辑类别 + + src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html + 1 + + Editing category dialog title + + + Paused + 暂停 + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 7 + + Paused subscription setting + + + No videos found. + 找不到视频 + + src/app/components/recent-videos/recent-videos.component.html + 38 + + No videos found + + + Reverse order + 倒序 + + src/app/dialogs/modify-playlist/modify-playlist.component.html + 14 + + Reverse order + + + Normal order + 正序 + + src/app/dialogs/modify-playlist/modify-playlist.component.html + 13 + + Normal order + + + Add content + 添加内容 + + src/app/dialogs/modify-playlist/modify-playlist.component.html + 19 + + Add content + + + Category: + 类别: + + src/app/dialogs/video-info-dialog/video-info-dialog.component.html + 29 + + Category property + + + (Paused) + (暂停) + + src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 1 + + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 1 + + + src/app/subscriptions/subscriptions.component.html + 12 + + + src/app/subscriptions/subscriptions.component.html + 31 + + + src/app/subscription/subscription/subscription.component.html + 5 + + Paused suffix + + + Auto-generated + 自动生成的 + + src/app/components/unified-file-card/unified-file-card.component.html + 5 + + Auto-generated label + + + + From b978007472aed6650c0f5475c803d72527ce8e20 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Jan 2021 10:25:29 +0000 Subject: [PATCH 015/680] Bump axios from 0.21.0 to 0.21.1 in /backend Bumps [axios](https://github.com/axios/axios) from 0.21.0 to 0.21.1. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v0.21.1/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v0.21.0...v0.21.1) Signed-off-by: dependabot[bot] --- backend/package-lock.json | 12 ++++++------ backend/package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 27c6ef8..4067dd9 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -253,9 +253,9 @@ "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" }, "axios": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz", - "integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", "requires": { "follow-redirects": "^1.10.0" } @@ -1081,9 +1081,9 @@ } }, "follow-redirects": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", - "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==" + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", + "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==" }, "forever-agent": { "version": "0.6.1", diff --git a/backend/package.json b/backend/package.json index 2c837c4..862a09c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -30,7 +30,7 @@ "dependencies": { "archiver": "^3.1.1", "async": "^3.1.0", - "axios": "^0.21.0", + "axios": "^0.21.1", "bcryptjs": "^2.4.0", "compression": "^1.7.4", "config": "^3.2.3", From db78e4ad5e5442299d6b16c88e933aa0974ac854 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sat, 9 Jan 2021 14:06:19 -0500 Subject: [PATCH 016/680] Fixed bug where playlist downloads would fail and progress would not show (for playlist downloads) --- backend/app.js | 15 +++++++++++---- backend/utils.js | 28 +++++++++++++++++----------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/backend/app.js b/backend/app.js index 6f51c45..bb55b2d 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1144,7 +1144,12 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) { } // store info in download for future use - download['_filename'] = info['_filename']; + if (Array.isArray(info)) { + download['fileNames'] = []; + for (let info_obj of info) download['fileNames'].push(info_obj['_filename']); + } else { + download['_filename'] = info['_filename']; + } download['filesize'] = utils.getExpectedFileSize(info); download_checker = setInterval(() => checkDownloadPercent(download), 1000); } @@ -1621,13 +1626,15 @@ function checkDownloadPercent(download) { be divided by the "total expected bytes." */ const file_id = download['file_id']; - const filename = path.format(path.parse(download['_filename'].substring(0, download['_filename'].length-4))); + // assume it's a playlist for logic reasons + const fileNames = Array.isArray(download['fileNames']) ? download['fileNames'] + : [path.format(path.parse(utils.removeFileExtension(download['_filename'])))]; const resulting_file_size = download['filesize']; if (!resulting_file_size) return; - glob(`${filename}*`, (err, files) => { - let sum_size = 0; + let sum_size = 0; + glob(`{${fileNames.join(',')}, }*`, (err, files) => { files.forEach(file => { try { const file_stats = fs.statSync(file); diff --git a/backend/utils.js b/backend/utils.js index 3524041..cd7c23d 100644 --- a/backend/utils.js +++ b/backend/utils.js @@ -105,20 +105,26 @@ function getDownloadedThumbnail(name, type, customPath = null) { return null; } -function getExpectedFileSize(info_json) { - if (info_json['filesize']) { - return info_json['filesize']; - } +function getExpectedFileSize(input_info_jsons) { + // treat single videos as arrays to have the file sizes checked/added to. makes the code cleaner + const info_jsons = Array.isArray(input_info_jsons) ? input_info_jsons : [input_info_jsons]; - const formats = info_json['format_id'].split('+'); let expected_filesize = 0; - formats.forEach(format_id => { - if (!info_json.formats) return expected_filesize; - info_json.formats.forEach(available_format => { - if (available_format.format_id === format_id && available_format.filesize) { - expected_filesize += available_format.filesize; - } + info_jsons.forEach(info_json => { + if (info_json['filesize']) { + expected_filesize += info_json['filesize']; + return; + } + const formats = info_json['format_id'].split('+'); + let individual_expected_filesize = 0; + formats.forEach(format_id => { + info_json.formats.forEach(available_format => { + if (available_format.format_id === format_id && available_format.filesize) { + individual_expected_filesize += available_format.filesize; + } + }); }); + expected_filesize += individual_expected_filesize; }); return expected_filesize; From ed1375d40bed41a4777a69160a5a0ba3d11be51c Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sat, 9 Jan 2021 14:07:43 -0500 Subject: [PATCH 017/680] Fixed bug where deleting videos while searching caused them to still show up in the UI --- src/app/components/recent-videos/recent-videos.component.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/components/recent-videos/recent-videos.component.ts b/src/app/components/recent-videos/recent-videos.component.ts index 68125cf..d130ccd 100644 --- a/src/app/components/recent-videos/recent-videos.component.ts +++ b/src/app/components/recent-videos/recent-videos.component.ts @@ -251,6 +251,9 @@ export class RecentVideosComponent implements OnInit { this.postsService.openSnackBar('Delete success!', 'OK.'); this.files.splice(file.index, 1); for (let i = 0; i < this.files.length; i++) { this.files[i].index = i } + if (this.search_mode) { + this.filterFiles(this.search_text); + } this.filterByProperty(this.filterProperty['property']); } else { this.postsService.openSnackBar('Delete failed!', 'OK.'); From a93aa080b317e4933db4593136dafdf4c03df56e Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sat, 9 Jan 2021 17:25:46 -0500 Subject: [PATCH 018/680] Fixed bug where playlistd could not be made --- backend/app.js | 4 ++-- backend/authentication/auth.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/app.js b/backend/app.js index bb55b2d..b49372e 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1954,7 +1954,7 @@ app.get('/api/getMp3s', optionalJwt, async function(req, res) { // get user audio files/playlists auth_api.passport.authenticate('jwt') mp3s = auth_api.getUserVideos(req.user.uid, 'audio'); - playlists = auth_api.getUserPlaylists(req.user.uid, 'audio'); + playlists = auth_api.getUserPlaylists(req.user.uid); } mp3s = JSON.parse(JSON.stringify(mp3s)); @@ -1975,7 +1975,7 @@ app.get('/api/getMp4s', optionalJwt, async function(req, res) { // get user videos/playlists auth_api.passport.authenticate('jwt') mp4s = auth_api.getUserVideos(req.user.uid, 'video'); - playlists = auth_api.getUserPlaylists(req.user.uid, 'video'); + playlists = auth_api.getUserPlaylists(req.user.uid); } mp4s = JSON.parse(JSON.stringify(mp4s)); diff --git a/backend/authentication/auth.js b/backend/authentication/auth.js index 442d7fa..e7cf337 100644 --- a/backend/authentication/auth.js +++ b/backend/authentication/auth.js @@ -285,7 +285,7 @@ exports.adminExists = function() { exports.getUserVideos = function(user_uid, type) { const user = users_db.get('users').find({uid: user_uid}).value(); - return type ? user['files'].filter(file => file.isAudio = (type === 'audio')) : user['files']; + return type ? user['files'].filter(file => file.isAudio === (type === 'audio')) : user['files']; } exports.getUserVideo = function(user_uid, file_uid, requireSharing = false) { From 95bb69f16b0e076f52ebe761a7f09a19788f14ae Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 10 Jan 2021 17:14:10 -0500 Subject: [PATCH 019/680] Fixed bug where videos would not delete in single-user mode --- backend/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app.js b/backend/app.js index b49372e..6f1b148 100644 --- a/backend/app.js +++ b/backend/app.js @@ -2615,7 +2615,7 @@ app.post('/api/deleteFile', optionalJwt, async (req, res) => { var wasDeleted = false; if (await fs.pathExists(fullpath)) { - wasDeleted = type === 'audio' ? await deleteAudioFile(name, path.basename(fullpath), blacklistMode) : await deleteVideoFile(name, path.basename(fullpath), blacklistMode); + wasDeleted = type === 'audio' ? await deleteAudioFile(name, path.dirname(fullpath), blacklistMode) : await deleteVideoFile(name, path.dirname(fullpath), blacklistMode); db.get('files').remove({uid: uid}).write(); wasDeleted = true; res.send(wasDeleted); From 7835185fe0ffb95271ec389c6c3cb5dc6da2ab87 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Mon, 11 Jan 2021 01:18:58 -0500 Subject: [PATCH 020/680] Made file card deletion much more reliable by finding out the index of the file on deletion rather than attempting to maintain a valid index --- .../recent-videos/recent-videos.component.ts | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/app/components/recent-videos/recent-videos.component.ts b/src/app/components/recent-videos/recent-videos.component.ts index d130ccd..f213e12 100644 --- a/src/app/components/recent-videos/recent-videos.component.ts +++ b/src/app/components/recent-videos/recent-videos.component.ts @@ -131,7 +131,6 @@ export class RecentVideosComponent implements OnInit { for (let i = 0; i < this.files.length; i++) { const file = this.files[i]; file.duration = typeof file.duration !== 'string' ? file.duration : this.durationStringToNumber(file.duration); - file.index = i; } if (this.search_mode) { this.filterFiles(this.search_text); @@ -239,22 +238,17 @@ export class RecentVideosComponent implements OnInit { const blacklistMode = args.blacklistMode; if (file.sub_id) { - this.deleteSubscriptionFile(file, index, blacklistMode); + this.deleteSubscriptionFile(file, blacklistMode); } else { - this.deleteNormalFile(file, index, blacklistMode); + this.deleteNormalFile(file, blacklistMode); } } - deleteNormalFile(file, index, blacklistMode = false) { + deleteNormalFile(file, blacklistMode = false) { this.postsService.deleteFile(file.uid, file.isAudio ? 'audio' : 'video', blacklistMode).subscribe(result => { if (result) { this.postsService.openSnackBar('Delete success!', 'OK.'); - this.files.splice(file.index, 1); - for (let i = 0; i < this.files.length; i++) { this.files[i].index = i } - if (this.search_mode) { - this.filterFiles(this.search_text); - } - this.filterByProperty(this.filterProperty['property']); + this.removeFileCard(file); } else { this.postsService.openSnackBar('Delete failed!', 'OK.'); } @@ -263,30 +257,40 @@ export class RecentVideosComponent implements OnInit { }); } - deleteSubscriptionFile(file, index, blacklistMode = false) { + deleteSubscriptionFile(file, blacklistMode = false) { if (blacklistMode) { - this.deleteForever(file, index); + this.deleteForever(file); } else { - this.deleteAndRedownload(file, index); + this.deleteAndRedownload(file); } } - deleteAndRedownload(file, index) { + deleteAndRedownload(file) { const sub = this.postsService.getSubscriptionByID(file.sub_id); this.postsService.deleteSubscriptionFile(sub, file.id, false, file.uid).subscribe(res => { this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`); - this.files.splice(index, 1); + this.removeFileCard(file); }); } - deleteForever(file, index) { + deleteForever(file) { const sub = this.postsService.getSubscriptionByID(file.sub_id); this.postsService.deleteSubscriptionFile(sub, file.id, true, file.uid).subscribe(res => { this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`); - this.files.splice(index, 1); + this.removeFileCard(file); }); } + removeFileCard(file_to_remove) { + console.log(file_to_remove.uid); + const index = this.files.map(e => e.uid).indexOf(file_to_remove.uid); + this.files.splice(index, 1); + if (this.search_mode) { + this.filterFiles(this.search_text); + } + this.filterByProperty(this.filterProperty['property']); + } + // sorting and filtering sortFiles(a, b) { From f0f2faa39806bbbe31456c6ab356e0426da5ec4b Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Mon, 11 Jan 2021 01:19:29 -0500 Subject: [PATCH 021/680] Sub's videos are removed from the post request when deleting a video as it's not needed --- src/app/posts.services.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 4ae90dd..f458494 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -365,6 +365,7 @@ export class PostsService implements CanActivate { } deleteSubscriptionFile(sub, file, deleteForever, file_uid) { + delete sub['videos']; return this.http.post(this.path + 'deleteSubscriptionFile', {sub: sub, file: file, deleteForever: deleteForever, file_uid: file_uid}, this.httpOptions) } From 539bc5094ac1ec5fbd0cf07296f8865282be809f Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Mon, 11 Jan 2021 01:20:07 -0500 Subject: [PATCH 022/680] Fixed bug where sometimes a subscription video's thumbnail would get deleted twice and throw an error --- backend/subscriptions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/subscriptions.js b/backend/subscriptions.js index 4bdca93..b3e2f1e 100644 --- a/backend/subscriptions.js +++ b/backend/subscriptions.js @@ -211,7 +211,7 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, var jsonPath = path.join(__dirname,filePath,name+'.info.json'); var videoFilePath = path.join(__dirname,filePath,name+ext); var imageFilePath = path.join(__dirname,filePath,name+'.jpg'); - var altImageFilePath = path.join(__dirname,filePath,name+'.jpg'); + var altImageFilePath = path.join(__dirname,filePath,name+'.webp'); const [jsonExists, videoFileExists, imageFileExists, altImageFileExists] = await Promise.all([ fs.pathExists(jsonPath), From a78f4e99d07db47c088e3aadf3da14aada7b0401 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Mon, 11 Jan 2021 01:20:53 -0500 Subject: [PATCH 023/680] Removed trivial browser log that occured at file deletion --- src/app/components/recent-videos/recent-videos.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/components/recent-videos/recent-videos.component.ts b/src/app/components/recent-videos/recent-videos.component.ts index f213e12..6aec7f2 100644 --- a/src/app/components/recent-videos/recent-videos.component.ts +++ b/src/app/components/recent-videos/recent-videos.component.ts @@ -282,7 +282,6 @@ export class RecentVideosComponent implements OnInit { } removeFileCard(file_to_remove) { - console.log(file_to_remove.uid); const index = this.files.map(e => e.uid).indexOf(file_to_remove.uid); this.files.splice(index, 1); if (this.search_mode) { From 133d84872964f5a79ace499770e31595cf92f9ef Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Mon, 11 Jan 2021 13:55:02 -0500 Subject: [PATCH 024/680] Fixed bug where deleting a file card wasn't possible if it was already deleted manually --- backend/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app.js b/backend/app.js index 6f1b148..5fab23c 100644 --- a/backend/app.js +++ b/backend/app.js @@ -2619,7 +2619,7 @@ app.post('/api/deleteFile', optionalJwt, async (req, res) => { db.get('files').remove({uid: uid}).write(); wasDeleted = true; res.send(wasDeleted); - } else if (video_obj) { + } else if (file_obj) { db.get('files').remove({uid: uid}).write(); wasDeleted = true; res.send(wasDeleted); From 28ee77cee0ac3cef25151b84f511649f192906ae Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Tue, 12 Jan 2021 16:42:30 -0500 Subject: [PATCH 025/680] Hotfix that allows playlists to be downloaded with categories --- backend/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app.js b/backend/app.js index 5fab23c..04edd6a 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1133,7 +1133,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) { return; } else if (info) { // check if it fits into a category. If so, then get info again using new downloadConfig - category = await categories_api.categorize(info); + if (!Array.isArray(info)) category = await categories_api.categorize(info); // set custom output if the category has one and re-retrieve info so the download manager has the right file name if (category && category['custom_output']) { From 1d5490c0ff9195bfcb8acf778cee6e17e3509e38 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Tue, 12 Jan 2021 22:08:42 -0500 Subject: [PATCH 026/680] Allows playlists to be categorized based on the first video that matches --- backend/app.js | 2 +- backend/appdata/default.json | 3 ++- backend/categories.js | 30 ++++++++++++++---------- backend/config.js | 3 ++- backend/consts.js | 4 ++++ src/app/settings/settings.component.html | 5 +++- src/assets/default.json | 3 ++- 7 files changed, 32 insertions(+), 18 deletions(-) diff --git a/backend/app.js b/backend/app.js index 04edd6a..1a65d47 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1133,7 +1133,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) { return; } else if (info) { // check if it fits into a category. If so, then get info again using new downloadConfig - if (!Array.isArray(info)) category = await categories_api.categorize(info); + if (!Array.isArray(info) || config_api.getConfigItem('ytdl_allow_playlist_categorization')) category = await categories_api.categorize(info); // set custom output if the category has one and re-retrieve info so the download manager has the right file name if (category && category['custom_output']) { diff --git a/backend/appdata/default.json b/backend/appdata/default.json index dca9eee..5bd5c54 100644 --- a/backend/appdata/default.json +++ b/backend/appdata/default.json @@ -20,7 +20,8 @@ "allow_quality_select": true, "download_only_mode": false, "allow_multi_download_mode": true, - "enable_downloads_manager": true + "enable_downloads_manager": true, + "allow_playlist_categorization": true }, "API": { "use_API_key": false, diff --git a/backend/categories.js b/backend/categories.js index d0b249a..d2af431 100644 --- a/backend/categories.js +++ b/backend/categories.js @@ -33,27 +33,31 @@ Rules: */ -async function categorize(file_json) { +async function categorize(file_jsons) { + // to make the logic easier, let's assume the file metadata is an array + if (!Array.isArray(file_jsons)) file_jsons = [file_jsons]; + let selected_category = null; const categories = getCategories(); if (!categories) { logger.warn('Categories could not be found. Initializing categories...'); db.assign({categories: []}).write(); return null; - return; } - for (let i = 0; i < categories.length; i++) { - const category = categories[i]; - const rules = category['rules']; - - // if rules for current category apply, then that is the selected category - if (applyCategoryRules(file_json, rules, category['name'])) { - selected_category = category; - logger.verbose(`Selected category ${category['name']} for ${file_json['webpage_url']}`); - return selected_category; - } - } + file_jsons.forEach(file_json => { + categories.forEach(category => { + const rules = category['rules']; + + // if rules for current category apply, then that is the selected category + if (applyCategoryRules(file_json, rules, category['name'])) { + selected_category = category; + logger.verbose(`Selected category ${category['name']} for ${file_json['webpage_url']}`); + return selected_category; + } + }); + }); + return selected_category; } diff --git a/backend/config.js b/backend/config.js index 4790e34..cb3e8b3 100644 --- a/backend/config.js +++ b/backend/config.js @@ -197,7 +197,8 @@ DEFAULT_CONFIG = { "allow_quality_select": true, "download_only_mode": false, "allow_multi_download_mode": true, - "enable_downloads_manager": true + "enable_downloads_manager": true, + "allow_playlist_categorization": true }, "API": { "use_API_key": false, diff --git a/backend/consts.js b/backend/consts.js index fa14171..fc29fcf 100644 --- a/backend/consts.js +++ b/backend/consts.js @@ -68,6 +68,10 @@ let CONFIG_ITEMS = { 'key': 'ytdl_enable_downloads_manager', 'path': 'YoutubeDLMaterial.Extra.enable_downloads_manager' }, + 'ytdl_allow_playlist_categorization': { + 'key': 'ytdl_allow_playlist_categorization', + 'path': 'YoutubeDLMaterial.Extra.allow_playlist_categorization' + }, // API 'ytdl_use_api_key': { diff --git a/src/app/settings/settings.component.html b/src/app/settings/settings.component.html index bd085b1..a7d69d6 100644 --- a/src/app/settings/settings.component.html +++ b/src/app/settings/settings.component.html @@ -140,7 +140,7 @@
-
+
Categories
@@ -154,6 +154,9 @@
+
+ Allow playlist categorization +
diff --git a/src/assets/default.json b/src/assets/default.json index 532b32e..ce2cdb3 100644 --- a/src/assets/default.json +++ b/src/assets/default.json @@ -20,7 +20,8 @@ "download_only_mode": false, "allow_multi_download_mode": true, "settings_pin_required": false, - "enable_downloads_manager": true + "enable_downloads_manager": true, + "allow_playlist_categorization": true }, "API": { "use_API_key": false, From d7d861ef0e504f6dc100fe2bd85f1fd5d1691d3b Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Tue, 12 Jan 2021 22:32:27 -0500 Subject: [PATCH 027/680] Fixed typo in default custom output key for categories --- backend/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app.js b/backend/app.js index 1a65d47..9dbad30 100644 --- a/backend/app.js +++ b/backend/app.js @@ -2244,7 +2244,7 @@ app.post('/api/createCategory', optionalJwt, async (req, res) => { name: name, uid: uuid(), rules: [], - custom_putput: '' + custom_output: '' }; db.get('categories').push(new_category).write(); From af58854f0e4e53add23a9b9ec2d401b22ad2f5ea Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 13 Jan 2021 12:50:18 -0500 Subject: [PATCH 028/680] Added info button to the player component --- src/app/player/player.component.html | 1 + src/app/player/player.component.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/src/app/player/player.component.html b/src/app/player/player.component.html index 6aaa7b0..086adf6 100644 --- a/src/app/player/player.component.html +++ b/src/app/player/player.component.html @@ -35,6 +35,7 @@ +
diff --git a/src/app/player/player.component.ts b/src/app/player/player.component.ts index 123e2b4..b3660a5 100644 --- a/src/app/player/player.component.ts +++ b/src/app/player/player.component.ts @@ -8,6 +8,7 @@ import { InputDialogComponent } from 'app/input-dialog/input-dialog.component'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { ShareMediaDialogComponent } from '../dialogs/share-media-dialog/share-media-dialog.component'; import { TwitchChatComponent } from 'app/components/twitch-chat/twitch-chat.component'; +import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component'; export interface IMedia { title: string; @@ -478,6 +479,15 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { } }); } + + openFileInfoDialog() { + this.dialog.open(VideoInfoDialogComponent, { + data: { + file: this.db_file, + }, + minWidth: '50vw' + }) + } // snackbar helper public openSnackBar(message: string, action: string) { From 6481102e01ab96e1326e70a0e9adb77632e74123 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 13 Jan 2021 16:12:11 -0500 Subject: [PATCH 029/680] Changes forEach loops in categorize() to regular for loops to facilitate early breaking --- backend/categories.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/categories.js b/backend/categories.js index d2af431..d4b19f5 100644 --- a/backend/categories.js +++ b/backend/categories.js @@ -45,8 +45,10 @@ async function categorize(file_jsons) { return null; } - file_jsons.forEach(file_json => { - categories.forEach(category => { + for (let i = 0; i < file_jsons.length; i++) { + const file_json = file_jsons[i]; + for (let j = 0; j < categories.length; j++) { + const category = categories[i]; const rules = category['rules']; // if rules for current category apply, then that is the selected category @@ -55,8 +57,8 @@ async function categorize(file_jsons) { logger.verbose(`Selected category ${category['name']} for ${file_json['webpage_url']}`); return selected_category; } - }); - }); + } + } return selected_category; } From a1b32e2851824b1b644e9ee7a21e5f201b764d42 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 20 Jan 2021 08:32:16 -0500 Subject: [PATCH 030/680] Added yt-dlp support Simplified update youtube-dl code --- backend/app.js | 58 +++++++++++++++++------- src/app/settings/settings.component.html | 3 +- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/backend/app.js b/backend/app.js index 04edd6a..209906e 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1659,11 +1659,24 @@ async function startYoutubeDL() { // auto updates the underlying youtube-dl binary, not YoutubeDL-Material async function autoUpdateYoutubeDL() { + const download_sources = { + 'youtube-dl': { + 'tags_url': 'https://api.github.com/repos/ytdl-org/youtube-dl/tags', + 'func': downloadLatestYoutubeDLBinary + }, + 'youtube-dlc': { + 'tags_url': 'https://api.github.com/repos/blackjack4494/yt-dlc/tags', + 'func': downloadLatestYoutubeDLCBinary + }, + 'yt-dlp': { + 'tags_url': 'https://api.github.com/repos/pukkandan/yt-dlp/tags', + 'func': downloadLatestYoutubeDLPBinary + } + } return new Promise(async resolve => { const default_downloader = config_api.getConfigItem('ytdl_default_downloader'); const using_youtube_dlc = default_downloader === 'youtube-dlc'; - const youtube_dl_tags_url = 'https://api.github.com/repos/ytdl-org/youtube-dl/tags' - const youtube_dlc_tags_url = 'https://api.github.com/repos/blackjack4494/yt-dlc/tags' + const tags_url = download_sources[default_downloader]['tags_url']; // get current version let current_app_details_path = 'node_modules/youtube-dl/bin/details'; let current_app_details_exists = fs.existsSync(current_app_details_path); @@ -1674,6 +1687,7 @@ async function autoUpdateYoutubeDL() { } let current_app_details = JSON.parse(fs.readFileSync(current_app_details_path)); let current_version = current_app_details['version']; + let current_downloader = current_app_details['downloader']; let stored_binary_path = current_app_details['path']; if (!stored_binary_path || typeof stored_binary_path !== 'string') { // logger.info(`INFO: Failed to get youtube-dl binary path at location: ${current_app_details_path}, attempting to guess actual path...`); @@ -1690,15 +1704,9 @@ async function autoUpdateYoutubeDL() { } // got version, now let's check the latest version from the youtube-dl API - let youtubedl_api_path = using_youtube_dlc ? youtube_dlc_tags_url : youtube_dl_tags_url; - if (default_downloader === 'youtube-dl') { - await downloadLatestYoutubeDLBinary('unknown', 'unknown'); - resolve(true); - return; - } - fetch(youtubedl_api_path, {method: 'Get'}) + fetch(tags_url, {method: 'Get'}) .then(async res => res.json()) .then(async (json) => { // check if the versions are different @@ -1708,16 +1716,16 @@ async function autoUpdateYoutubeDL() { return false; } const latest_update_version = json[0]['name']; - if (current_version !== latest_update_version) { - // versions different, download new update + if (current_version !== latest_update_version || default_downloader !== current_downloader) { + // versions different or different downloader is being used, download new update logger.info(`Found new update for ${default_downloader}. Updating binary...`); try { await checkExistsWithTimeout(stored_binary_path, 10000); } catch(e) { logger.error(`Failed to update ${default_downloader} - ${e}`); } - if (using_youtube_dlc) await downloadLatestYoutubeDLCBinary(latest_update_version); - else await downloadLatestYoutubeDLBinary(current_version, latest_update_version); + + await download_sources[default_downloader]['func'](latest_update_version); resolve(true); } else { @@ -1731,7 +1739,7 @@ async function autoUpdateYoutubeDL() { }); } -async function downloadLatestYoutubeDLBinary(current_version, new_version) { +async function downloadLatestYoutubeDLBinary() { return new Promise(resolve => { let binary_path = 'node_modules/youtube-dl/bin'; downloader(binary_path, function error(err, done) { @@ -1741,6 +1749,7 @@ async function downloadLatestYoutubeDLBinary(current_version, new_version) { resolve(false); } logger.info(`youtube-dl successfully updated!`); + updateDetailsJSON(null, 'youtube-dl'); resolve(true); }); }); @@ -1754,10 +1763,25 @@ async function downloadLatestYoutubeDLCBinary(new_version) { await fetchFile(download_url, output_path, `youtube-dlc ${new_version}`); - const details_path = 'node_modules/youtube-dl/bin/details'; - const details_json = fs.readJSONSync('node_modules/youtube-dl/bin/details'); - details_json['version'] = new_version; + updateDetailsJSON(new_version, 'youtube-dlc'); +} +async function downloadLatestYoutubeDLPBinary(new_version) { + const file_ext = is_windows ? '.exe' : ''; + + const download_url = `https://github.com/pukkandan/yt-dlp/releases/latest/download/youtube-dlc${file_ext}`; + const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`; + + await fetchFile(download_url, output_path, `yt-dlp ${new_version}`); + + updateDetailsJSON(new_version, 'yt-dlp'); +} + +function updateDetailsJSON(new_version, downloader) { + const details_path = 'node_modules/youtube-dl/bin/details'; + const details_json = fs.readJSONSync(details_path); + if (new_version) details_json['version'] = new_version; + details_json['downloader'] = downloader; fs.writeJSONSync(details_path, details_json); } diff --git a/src/app/settings/settings.component.html b/src/app/settings/settings.component.html index bd085b1..fe7d81c 100644 --- a/src/app/settings/settings.component.html +++ b/src/app/settings/settings.component.html @@ -286,8 +286,9 @@ Select a downloader - youtube-dlc youtube-dl + youtube-dlc + yt-dlp
From 00a0ab460bbf59955c56651d52788cb9e0a1f433 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 20 Jan 2021 08:37:14 -0500 Subject: [PATCH 031/680] Subscription's videos are now stripped from HTTP requests where they are not needed --- src/app/posts.services.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index f458494..6657f00 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -357,10 +357,12 @@ export class PostsService implements CanActivate { } updateSubscription(subscription) { + delete subscription['videos']; return this.http.post(this.path + 'updateSubscription', {subscription: subscription}, this.httpOptions); } unsubscribe(sub, deleteMode = false) { + delete sub['videos']; return this.http.post(this.path + 'unsubscribe', {sub: sub, deleteMode: deleteMode}, this.httpOptions) } From 3f9314a0c3e1d707d66b370c2f3e7bea83bb0daf Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Thu, 28 Jan 2021 22:11:04 -0500 Subject: [PATCH 032/680] Fixed bug where categories selection logic had an out of range exception --- backend/categories.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/categories.js b/backend/categories.js index d4b19f5..2134373 100644 --- a/backend/categories.js +++ b/backend/categories.js @@ -48,7 +48,7 @@ async function categorize(file_jsons) { for (let i = 0; i < file_jsons.length; i++) { const file_json = file_jsons[i]; for (let j = 0; j < categories.length; j++) { - const category = categories[i]; + const category = categories[j]; const rules = category['rules']; // if rules for current category apply, then that is the selected category From e34aa4d9d6aaea9bb2337f03465513828c16af00 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 31 Jan 2021 19:47:14 -0500 Subject: [PATCH 033/680] Adds Dutch language support --- src/app/settings/locales_list.ts | 2 +- src/app/settings/settings.component.ts | 2 +- src/assets/i18n/messages.nl.json | 248 +++ src/assets/i18n/messages.nl.xlf | 2517 ++++++++++++++++++++++++ 4 files changed, 2767 insertions(+), 2 deletions(-) create mode 100644 src/assets/i18n/messages.nl.json create mode 100644 src/assets/i18n/messages.nl.xlf diff --git a/src/app/settings/locales_list.ts b/src/app/settings/locales_list.ts index 5e88b73..47d398c 100644 --- a/src/app/settings/locales_list.ts +++ b/src/app/settings/locales_list.ts @@ -159,7 +159,7 @@ export const isoLangs = { }, 'nl': { 'name': 'Dutch', - 'nativeName': 'Nederlands, Vlaams' + 'nativeName': 'Nederlands' }, 'en': { 'name': 'English', diff --git a/src/app/settings/settings.component.ts b/src/app/settings/settings.component.ts index 824b270..2ef36fa 100644 --- a/src/app/settings/settings.component.ts +++ b/src/app/settings/settings.component.ts @@ -20,7 +20,7 @@ import { EditCategoryDialogComponent } from 'app/dialogs/edit-category-dialog/ed }) export class SettingsComponent implements OnInit { all_locales = isoLangs; - supported_locales = ['en', 'es', 'de', 'fr', 'zh', 'nb', 'it', 'en-GB']; + supported_locales = ['en', 'es', 'de', 'fr', 'nl', 'zh', 'nb', 'it', 'en-GB']; initialLocale = localStorage.getItem('locale'); initial_config = null; diff --git a/src/assets/i18n/messages.nl.json b/src/assets/i18n/messages.nl.json new file mode 100644 index 0000000..79226a8 --- /dev/null +++ b/src/assets/i18n/messages.nl.json @@ -0,0 +1,248 @@ +{ + "004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Over", + "994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Profiel", + "adb4562d2dbd3584370e44496969d58c511ecb63": "Donker", + "121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Instellingen", + "92eee6be6de0b11c924e3ab27db30257159c0a7c": "Overzicht", + "6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Inloggen", + "357064ca9d9ac859eb618e28e8126fa32be049e2": "Abonnementen", + "822fab38216f64e8166d368b59fe756ca39d301b": "Downloads", + "4a9889d36910edc8323d7bab60858ab3da6d91df": "Alleen audio", + "6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Downloaden", + "a38ae1082fec79ba1f379978337385a539a28e73": "Kwaliteit", + "4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "URL gebruiken", + "d3f02f845e62cebd75fde451ab8479d2a8ad784d": "Bekijken", + "96a01fafe135afc58b0f8071a4ab00234495ce18": "Meerdere video's downloaden", + "6a3777f913cf3f288664f0632b9f24794fdcc24e": "Afbreken", + "322ed150e02666fe2259c5b4614eac7066f4ffa0": "Geavanceerd", + "4e4c721129466be9c3862294dc40241b64045998": "Aanvullende opties toekennen", + "ad2f8ac8b7de7945b80c8e424484da94e597125f": "Aanvullende opties", + "a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "Je hoeft alleen de aanvullende opties op te geven, dus niet de url. Je kunt de opties scheiden met twee komma's: ,,", + "3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Aangepaste uitvoer gebruiken", + "d9c02face477f2f9cdaae318ccee5f89856851fb": "Aangepaste uitvoer", + "fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Documentatie", + "19d1ae64d94d28a29b2c57ae8671aace906b5401": "Het pad is relatief aan het ingestelde downloadpad. Laat de extensie achterwege.", + "b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Geteste opdracht:", + "8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "Authenticatie gebruiken", + "08c74dc9762957593b91f6eb5d65efdfc975bf48": "Gebruikersnaam", + "c32ef07f8803a223a83ed17024b38e8d82292407": "Wachtwoord", + "17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Afspeellijst maken", + "cff1428d10d59d14e45edec3c735a27b5482db59": "Naam", + "f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "Soort", + "f0baeb8b69d120073b6d60d34785889b0c3232c8": "Audio", + "2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "Video", + "f47e2d56dd8a145b2e9599da9730c049d52962a2": "Audiobestanden", + "a52dae09be10ca3a65da918533ced3d3f4992238": "Video's", + "a9806cf78ce00eb2613eeca11354a97e033377b8": "Abonneren op afspeellijst of kanaal", + "801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL", + "93efc99ae087fc116de708ecd3ace86ca237cf30": "De url van de afspeellijst of het kanaal", + "08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Aangepaste naam", + "ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Alle uploads downloaden", + "d641b8fa5ac5e85114c733b1f7de6976bd091f70": "Maximumkwaliteit", + "c76a955642714b8949ff3e4b4990864a2e2cac95": "Audiomodus", + "408ca4911457e84a348cecf214f02c69289aa8f1": "Streamingmodus", + "f432e1a8d6adb12e612127978ce2e0ced933959c": "Deze worden toegevoegd ná de standaardopties.", + "98b6ec9ec138186d663e64770267b67334353d63": "Aangepaste bestandsuitvoer", + "d7b35c384aecd25a516200d6921836374613dfe7": "Annuleren", + "d0336848b0c375a1c25ba369b3481ee383217a4f": "Abonneren", + "28a678e9cabf86e44c32594c43fa0e890135c20f": "Video's downloaden die geüpload zijn in de afgelopen", + "e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Soort:", + "c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL:", + "ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID:", + "f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Sluiten", + "8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Archief exporteren", + "3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "De-abonneren", + "303e45ffae995c9817e510e38cb969e6bb3adcbf": "(onderbroken)", + "a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Archief:", + "616e206cb4f25bd5885fc35925365e43cf5fb929": "Naam:", + "c6eb45d085384903e53ab001a3513d1de6a1dbac": "Uploader:", + "109c6f4a5e46efb933612ededfaf52a13178b7e0": "Bestandsgrootte:", + "bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Pad:", + "a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Uploaddatum:", + "0cc1dec590ecd74bef71a865fb364779bc42a749": "Categorie:", + "d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "youtube-dl-opties aanpassen", + "7fc1946abe2b40f60059c6cd19975d677095fd19": "Geteste nieuwe aanvullende opties", + "0b71824ae71972f236039bed43f8d2323e8fd570": "Optie toevoegen", + "c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Zoeken op categorie", + "9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Optiewaarde gebruiken", + "7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Optie toevoegen", + "b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Aanpassen", + "25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Optiewaarde", + "91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Updater", + "b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Gebruikersregistratie", + "024886ca34a6f309e3e51c2ed849320592c3faaa": "Gebruikersnaam", + "cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registreren", + "ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Nieuwe cookies uploaden", + "a8b7b9c168fd936a75e500806a8c0d7755ef1198": "Let op: de nieuwe cookies overschrijven de oude. Daarnaast zijn de cookies procesgebonden en niet gebruikersgebonden.", + "98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Slepen-en-neerzetten", + "4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Afspeellijst aanpassen", + "5caadefa4143cf6766a621b0f54f91f373a1f164": "Inhoud toevoegen", + "52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Opslaan", + "33026f57ea65cd9c8a5d917a08083f71a718933a": "Normale volgorde", + "29376982b1205d9d6ea3d289e8e2f8e1ac2839b1": "Omgekeerde volgorde", + "d02888c485d3aeab6de628508f4a00312a722894": "Mijn video's", + "7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Zoeken", + "73423607944a694ce6f9e55cfee329681bb4d9f9": "Geen video's gevonden.", + "3697f8583ea42868aa269489ad366103d94aece7": "Bewerken", + "07db550ae114d9faad3a0cbb68bcc16ab6cd31fc": "Onderbroken", + "c3b0b86523f1d10e84a71f9b188d54913a11af3b": "Categorie bewerken", + "2489eefea00931942b91f4a1ae109514b591e2e1": "Regels", + "e4eeb9106dbcbc91ca1ac3fb4068915998a70f37": "Regel toevoegen", + "792dc6a57f28a1066db283f2e736484f066005fd": "Twitch-chatgesprek downloaden", + "28f86ffd419b869711aa13f5e5ff54be6d70731c": "Aanpassen", + "826b25211922a1b46436589233cb6f1a163d89b7": "Verwijderen", + "321e4419a943044e674beb55b8039f42a9761ca5": "Informatie", + "e684046d73bcee88e82f7ff01e2852789a05fc32": "Aantal:", + "34504b488c24c27e68089be549f0eeae6ebaf30b": "Verwijderen en op zwarte lijst plaatsen", + "dad95154dcef3509b8cc705046061fd24994bbb7": "weergaven", + "5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Aanpassingen opslaan", + "4d8a18b04a1f785ecd8021ac824e0dfd5881dbfc": "Het downloaden is voltooid", + "348cc5d553b18e862eb1c1770e5636f6b05ba130": "Er is een fout opgetreden", + "4f8b2bb476981727ab34ed40fde1218361f92c45": "Details", + "e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "Er is een fout opgetreden:", + "77b0c73840665945b25bd128709aa64c8f017e1c": "Gestart om:", + "08ff9375ec078065bcdd7637b7ea65fce2979266": "Afgerond om:", + "ad127117f9471612f47d01eae09709da444a36a4": "Bestandspad(en):", + "e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "Mijn abonnementen", + "807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Kanalen", + "47546e45bbb476baaaad38244db444c427ddc502": "Afspeellijsten", + "29b89f751593e1b347eef103891b7a1ff36ec03f": "De naam is niet beschikbaar omdat het kanaal nog wordt opgehaald.", + "4636cd4a1379c50d471e98786098c4d39e1e82ad": "Je hebt geen abonnementen.", + "2e0a410652cb07d069f576b61eab32586a18320d": "De naam is niet beschikbaar omdat de afspeellijst nog wordt opgehaald.", + "587b57ced54965d8874c3fd0e9dfedb987e5df04": "Je hebt geen abonnementen.", + "82421c3e46a0453a70c42900eab51d58d79e6599": "Algemeen", + "0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Downloader", + "d5f69691f9f05711633128b5a3db696783266b58": "Diversen", + "bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Geavanceerd", + "4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Gebruikers", + "eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Logboeken", + "fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Close} false {Cancel} other {otha}}", + "54c512cca1923ab72faf1a0bd98d3d172469629a": "De url waarvan deze app wordt geladen, zonder het poortnummer.", + "cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Poort", + "22e8f1d0423a3b784fe40fab187b92c06541b577": "Het gewenste poortnummer (standaard: 17442).", + "d4477669a560750d2064051a510ef4d7679e2f3e": "Meerdere gebruikers", + "2eb03565fcdce7a7a67abc277a936a32fcf51557": "Gebruikersbasispad", + "a64505c41150663968e277ec9b3ddaa5f4838798": "Het basispad voor gebruikers en hun gedownloade video's.", + "4e3120311801c4acd18de7146add2ee4a4417773": "Abonnementen toestaan", + "4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Abonnementenbasispad", + "bc9892814ee2d119ae94378c905ea440a249b84a": "Het basispad voor video's van afspeellijsten en kanalen uit je abonnementen. Dit is relatief aan YTDL-Material's hoofdmap.", + "5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Controletussenpoos", + "0f56a7449b77630c114615395bbda4cab398efd8": "In seconden (alleen cijfers).", + "13759b09a7f4074ceee8fa2f968f9815fdf63295": "Soms worden nieuwe video's gedownload voordat ze volledig verwerkt zijn. Met deze instelling wordt de volgende dag gecontroleerd of er een hogere kwaliteit beschikbaar is.", + "3d1a47dc18b7bd8b5d9e1eb44b235ed9c4a2b513": "Nieuwe uploads opnieuw downloaden", + "27a56aad79d8b61269ed303f11664cc78bcc2522": "Thema", + "ff7cee38a2259526c519f878e71b964f41db4348": "Standaard", + "7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Themawijziging toestaan", + "fe46ccaae902ce974e2441abe752399288298619": "Taal", + "ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Audiopad", + "c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Het pad voor audiodownloads. Dit is relatief aan YTDL-Material's hoofdmap.", + "46826331da1949bd6fb74624447057099c9d20cd": "Videomap", + "17c92e6d47a213fa95b5aa344b3f258147123f93": "Het pad voor videodownloads. Dit is relatief aan YTDL-Material's hoofdmap.", + "cfe829634b1144bc44b6d38cf5584ea65db9804f": "Standaard bestandsuitvoer", + "1148fd45287ff09955b938756bc302042bcb29c7": "Dit pad is relatief aan bovenstaande downloadpaden. Laat de extensie achterwege.", + "ef418d4ece7c844f3a5e431da1aa59bedd88da7b": "Algemene aanvullende opties", + "6b995e7130b4d667eaab6c5f61b362ace486d26d": "Algemene aanvullende opties voor downloads op de overzichtspagina. Scheidt deze met komma's: ,,", + "04201f9d27abd7d6f58a4328ab98063ce1072006": "Categorieën", + "78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "youtube-dl-archief gebruiken", + "ffc19f32b1cba0daefc0e5668f89346db1db83ad": "Miniatuurvoorbeeld opslaan", + "384de8f8f112c9e6092eb2698706d391553f3e8d": "Metagegevens opslaan", + "fb35145bfb84521e21b6385363d59221f436a573": "Alle downloads afbreken", + "61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Boventitel", + "78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Bestandsbeheer ingeschakeld", + "a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Downloadbeheer ingeschakeld", + "c33bd5392b39dbed36b8e5a1145163a15d45835f": "Kwaliteitskeuze toestaan", + "bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Downloadmodus", + "09d31c803a7252658694e1e3176b97f5655a3fe3": "Meerdere downloads toestaan", + "1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Openbare api gebruiken", + "23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Openbare api-sleutel", + "41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Documentatie bekijken", + "00a94f58d9eb2e3aa561440eabea616d0c937fa2": "Let op: hiermee verwijder je je oude api-sleutel!", + "1b258b258b4cc475ceb2871305b61756b0134f4a": "Genereren", + "d5d7c61349f3b0859336066e6d453fc35d334fe5": "YouTube-api gebruiken", + "ce10d31febb3d9d60c160750570310f303a22c22": "YouTube-api-sleutel", + "8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "Het genereren van een sleutel is eenvoudig.", + "d162f9fcd6a7187b391e004f072ab3da8377c47d": "Twitch-api gebruiken", + "8ae23bc4302a479f687f4b20a84c276182e2519c": "Twitch-api-sleutel", + "84ffcebac2709ca0785f4a1d5ba274433b5beabc": "Ook wel de client-id.", + "5fb1e0083c9b2a40ac8ae7dcb2618311c291b8b9": "Twitch-chatgesprekken automatisch downloaden", + "9b3cedfa83c6d7acb3210953289d1be4aab115c7": "Klik hier", + "7f09776373995003161235c0c8d02b7f91dbc4df": "om de officiële Chrome-extensie van YouTubeDL-Material te downloaden.", + "5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Hiervoor dien je de extensie handmatig te laden en de frontend-url op te geven in de instellingen.", + "9a2ec6da48771128384887525bdcac992632c863": "om de officiële Firefox-extensie van YouTubeDL-Material te installeren.", + "eb81be6b49e195e5307811d1d08a19259d411f37": "Uitgebreide installatiehandleiding.", + "cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "Je hoeft alleen de frontend-url op te geven in de instellingen.", + "61b81b11aad0b9d970ece2fce18405f07eac69c2": "Sleep de link naar je bladwijzers en klaar is Kees! Ga vervolgens naar een YouTube-video en klik op de bladwijzer.", + "c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "Audio-bookmarklet genereren", + "ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Kies een downloader", + "5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Standaard downloadagent gebruiken", + "c776eb4992b6c98f58cd89b20c1ea8ac37888521": "Kies een downloadagent", + "0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "Logniveau", + "db6c192032f4cab809aad35215f0aa4765761897": "Inlogverloopdatum", + "dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Geavanceerd downloaden toestaan", + "431e5f3a0dde88768d1074baedd65266412b3f02": "Cookies gebruiken", + "80651a7ad1229ea6613557d3559f702cfa5aecf5": "Cookies instellen", + "37224420db54d4bc7696f157b779a7225f03ca9d": "Gebruikersregistratie toestaan", + "fa548cee6ea11c160a416cac3e6bdec0363883dc": "Authenticatiemethode", + "4f56ced9d6b85aeb1d4346433361d47ea72dac1a": "Intern", + "e3d7c5f019e79a3235a28ba24df24f11712c7627": "LDAP", + "1db9789b93069861019bd0ccaa5d4706b00afc61": "LDAP-url", + "f50fa6c09c8944aed504f6325f2913ee6c7a296a": "Bind DN", + "080cc6abcba236390fc22e79792d0d3443a3bd2a": "Bind-inloggegevens", + "cfa67d14d84fe0e9fadf251dc51ffc181173b662": "Zoekdatabank", + "e01d54ecc1a0fcf9525a3c100ed8b83d94e61c23": "Zoekfilter", + "cec82c0a545f37420d55a9b6c45c20546e82f94e": "Over YouTubeDL-Material", + "199c17e5d6a419313af3c325f06dcbb9645ca618": "is een opensource YouTube-downloader, gebouwd volgens Google's Material Design-specificaties. Je kunt naadloos je favoriete video's downloaden als audio- of videobestanden of abonneren op je favoriete kanalen of afspeellijsten om altijd de nieuwste video's binnen te halen.", + "bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "bevat een aantal handige functies, zoals een uitgebreide api, Docker-ondersteuning en is volledig vertaalbaar. Meer functies zijn te vinden op onze GitHub-pagina (klik op het GitHub-pictogram).", + "a45e3b05f0529dc5246d70ef62304c94426d4c81": "Geïnstalleerde versie:", + "b33536f59b94ec935a16bd6869d836895dc5300c": "Heb je een bug aangetroffen of een idee?", + "e1f398f38ff1534303d4bb80bd6cece245f24016": "om een 'issue' te openen!", + "e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Bezig met controleren op updates...", + "a16e92385b4fd9677bb830a4b796b8b79c113290": "Update beschikbaar", + "189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Je kunt de update installeren via het instellingenmenu.", + "1372e61c5bd06100844bd43b98b016aabc468f62": "Kies een versie:", + "1f6d14a780a37a97899dc611881e6bc971268285": "Delen toestaan", + "6580b6a950d952df847cb3d8e7176720a740adc8": "Tijdstempel gebruiken", + "4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "seconden", + "3a6e5a6aa78ca864f6542410c5dafb6334538106": "Kopiëren naar klembord", + "a249a5ae13e0835383885aaf697d2890cc3e53e9": "Afspeellijst delen", + "15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Video delen", + "1d540dcd271b316545d070f9d182c372d923aadd": "Audio delen", + "a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "Sessie-id:", + "b6c453e0e61faea184bbaf5c5b0a1e164f4de2a2": "Alle downloads wissen", + "eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(huidig)", + "7117fc42f860e86d983bfccfcf2654e5750f3406": "Geen downloads beschikbaar!", + "42ff677ec14f111e88bd6cdd30145378e994d1bf": "Mijn profiel", + "bb694b49d408265c91c62799c2b3a7e3151c824d": "Uitloggen", + "ac9d09de42edca1296371e4d801349c9096ac8de": "UID:", + "a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Aangemaakt:", + "fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Je bent niet ingelogd.", + "a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Beheerdersaccount aanmaken", + "2d2adf3ca26a676bca2269295b7455a26fd26980": "Er zijn geen beheerdersaccounts aangetroffen. Hiermee maak je een beheerdersaccount met wachtwoord aan - de gebruikersnaam is 'admin'.", + "70a67e04629f6d412db0a12d51820b480788d795": "Aanmaken", + "4d92a0395dd66778a931460118626c5794a3fc7a": "Gebruikers toevoegen", + "b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Rol aanpassen", + "746f64ddd9001ac456327cd9a3d5152203a4b93c": "Gebruikersnaam", + "52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Rol", + "59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Acties", + "2bd201aea09e43fbfd3cd15ec0499b6755302329": "Gebruiker beheren", + "95b95a9c79e4fd9ed41f6855e37b3b06af25bcab": "Gebruiker verwijderen", + "632e8b20c98e8eec4059a605a4b011bb476137af": "Gebruiker bewerken", + "29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "Gebruikers-uid:", + "e70e209561583f360b1e9cefd2cbb1fe434b6229": "Nieuw wachtwoord", + "6498fa1b8f563988f769654a75411bb8060134b9": "Nieuw wachtwoord instellen", + "544e09cdc99a8978f48521d45f62db0da6dcf742": "Standaardrol gebruiken", + "4f20f2d5a6882190892e58b85f6ccbedfa737952": "Ja", + "3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "Nee", + "57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Rol beheren", + "5009630cdf32ab4f1c78737b9617b8773512c05a": "Aantal regels:", + "8a0bda4c47f10b2423ff183acefbf70d4ab52ea2": "Logboeken wissen", + "24dc3ecf7ec2c2144910c4f3d38343828be03a4c": "Automatisch gegenereerd", + "ccf5ea825526ac490974336cb5c24352886abc07": "Bestand openen", + "5656a06f17c24b2d7eae9c221567b209743829a9": "Bestand openen op nieuw tabblad", + "a0720c36ee1057e5c54a86591b722485c62d7b1a": "Ga naar abonnement", + "94e01842dcee90531caa52e4147f70679bac87fe": "Verwijderen en opnieuw downloaden", + "2031adb51e07a41844e8ba7704b054e98345c9c1": "Permanent verwijderen", + "ddc31f2885b1b33a7651963254b0c197f2a64086": "Meer tonen.", + "56a2a773fbd5a6b9ac2e6b89d29d70a2ed0f3227": "Minder tonen.", + "2054791b822475aeaea95c0119113de3200f5e1c": "Duur:" +} \ No newline at end of file diff --git a/src/assets/i18n/messages.nl.xlf b/src/assets/i18n/messages.nl.xlf new file mode 100644 index 0000000..0049336 --- /dev/null +++ b/src/assets/i18n/messages.nl.xlf @@ -0,0 +1,2517 @@ + + + + + + About + Over + + src/app/app.component.html + 32 + + About menu label + + + Profile + Profiel + + src/app/app.component.html + 19 + + Profile menu label + + + Dark + Donker + + src/app/app.component.html + 23 + + + src/app/settings/settings.component.html + 75 + + Dark mode toggle label + + + Settings + Instellingen + + src/app/app.component.html + 28 + + + src/app/settings/settings.component.html + 1 + + Settings menu label + + + Home + Overzicht + + src/app/app.component.html + 43 + + Navigation menu Home Page title + + + Login + Inloggen + + src/app/app.component.html + 44 + + + src/app/components/login/login.component.html + 15 + + + src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 20 + + Navigation menu Login Page title + + + Subscriptions + Abonnementen + + src/app/app.component.html + 45 + + Navigation menu Subscriptions Page title + + + Downloads + Downloads + + src/app/app.component.html + 46 + + Navigation menu Downloads Page title + + + Only Audio + Alleen audio + + src/app/main/main.component.html + 60,61 + + Only Audio checkbox + + + Download + Downloaden + + src/app/main/main.component.html + 74,75 + + Main download button + + + Quality + Kwaliteit + + src/app/main/main.component.html + 19,20 + + Quality select label + + + Use URL + URL gebruiken + + src/app/main/main.component.html + 46 + + YT search Use URL button for searched video + + + View + Bekijken + + src/app/main/main.component.html + 50,51 + + YT search View button for searched video + + + Multi-download Mode + Meerdere video's downloaden + + src/app/main/main.component.html + 65,66 + + Multi-download Mode checkbox + + + Cancel + Afbreken + + src/app/main/main.component.html + 79,80 + + Cancel download button + + + Advanced + Geavanceerd + + src/app/main/main.component.html + 91,92 + + Advanced download mode panel + + + Use custom args + Aanvullende opties toekennen + + src/app/main/main.component.html + 105,106 + + Use custom args checkbox + + + Custom args + Aanvullende opties + + src/app/main/main.component.html + 110 + + + src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 57 + + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 44 + + Custom args placeholder + + + No need to include URL, just everything after. Args are delimited using two commas like so: ,, + Je hoeft alleen de aanvullende opties op te geven, dus niet de url. Je kunt de opties scheiden met twee komma's: ,, + + src/app/main/main.component.html + 113,114 + + Custom Args input hint + + + Use custom output + Aangepaste uitvoer gebruiken + + src/app/main/main.component.html + 121,122 + + Use custom output checkbox + + + Custom output + Aangepaste uitvoer + + src/app/main/main.component.html + 125 + + Custom output placeholder + + + Documentation + Documentatie + + src/app/main/main.component.html + 127 + + + src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 69 + + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 56 + + + src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html + 47 + + + src/app/settings/settings.component.html + 125 + + Youtube-dl output template documentation link + + + Path is relative to the config download path. Don't include extension. + Het pad is relatief aan het ingestelde downloadpad. Laat de extensie achterwege. + + src/app/main/main.component.html + 128 + + + src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 70 + + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 57 + + + src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html + 48 + + Custom Output input hint + + + Simulated command: + Geteste opdracht: + + src/app/main/main.component.html + 97,98 + + Simulated command label + + + Use authentication + Authenticatie gebruiken + + src/app/main/main.component.html + 135,136 + + Use authentication checkbox + + + Username + Gebruikersnaam + + src/app/main/main.component.html + 139 + + YT Username placeholder + + + Password + Wachtwoord + + src/app/main/main.component.html + 144 + + + src/app/dialogs/add-user-dialog/add-user-dialog.component.html + 11 + + + src/app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html + 10 + + YT Password placeholder + + + Create a playlist + Afspeellijst maken + + src/app/create-playlist/create-playlist.component.html + 1 + + Create a playlist dialog title + + + Name + Naam + + src/app/create-playlist/create-playlist.component.html + 6 + + + src/app/dialogs/modify-playlist/modify-playlist.component.html + 7 + + + src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html + 5 + + Playlist name placeholder + + + Type + Soort + + src/app/create-playlist/create-playlist.component.html + 11 + + Type select + + + Audio + Audio + + src/app/create-playlist/create-playlist.component.html + 12 + + Audio + + + Video + Video + + src/app/create-playlist/create-playlist.component.html + 13 + + Video + + + Audio files + Audiobestanden + + src/app/create-playlist/create-playlist.component.html + 19 + + Audio files title + + + Videos + Video's + + src/app/create-playlist/create-playlist.component.html + 20 + + + src/app/subscription/subscription/subscription.component.html + 29 + + Videos title + + + Subscribe to playlist or channel + Abonneren op afspeellijst of kanaal + + src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 1 + + Subscribe dialog title + + + URL + URL + + src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 8 + + + src/app/settings/settings.component.html + 18 + + Subscription URL input placeholder + + + The playlist or channel URL + De url van de afspeellijst of het kanaal + + src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 9 + + Subscription URL input hint + + + Custom name + Aangepaste naam + + src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 19 + + Subscription custom name placeholder + + + Download all uploads + Alle uploads downloaden + + src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 23 + + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 10 + + Download all uploads subscription setting + + + Max quality + Maximumkwaliteit + + src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 40 + + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 32 + + Max quality placeholder + + + Audio-only mode + Audiomodus + + src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 47 + + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 27 + + Streaming-only mode + + + Streaming-only mode + Streamingmodus + + src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 52 + + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 39 + + Streaming-only mode + + + These are added after the standard args. + Deze worden toegevoegd ná de standaardopties. + + src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 60 + + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 47 + + Custom args hint + + + Custom file output + Aangepaste bestandsuitvoer + + src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 66 + + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 53 + + + src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html + 44 + + Subscription custom file output placeholder + + + Cancel + Annuleren + + src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 79 + + + src/app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 84 + + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 66 + + + src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html + 54 + + + src/app/components/modify-users/modify-users.component.html + 61 + + Subscribe cancel button + + + Subscribe + Abonneren + + src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 81 + + Subscribe button + + + Download videos uploaded in the last + Video's downloaden die geüpload zijn in de afgelopen + + src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html + 26 + + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 13 + + Download time range prefix + + + Type: + Soort: + + src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 5 + + Subscription type property + + + URL: + URL: + + src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 9 + + + src/app/dialogs/video-info-dialog/video-info-dialog.component.html + 9 + + Subscription URL property + + + ID: + ID: + + src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 13 + + + src/app/file-card/file-card.component.html + 7 + + + src/app/download-item/download-item.component.html + 4 + + Subscription ID property + + + Close + Sluiten + + src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 23 + + + src/app/dialogs/video-info-dialog/video-info-dialog.component.html + 35 + + + src/app/dialogs/update-progress-dialog/update-progress-dialog.component.html + 17 + + + src/app/dialogs/add-user-dialog/add-user-dialog.component.html + 18 + + + src/app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html + 40 + + + src/app/dialogs/about-dialog/about-dialog.component.html + 59 + + + src/app/dialogs/share-media-dialog/share-media-dialog.component.html + 30 + + + src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 27 + + + src/app/components/manage-user/manage-user.component.html + 30 + + + src/app/components/manage-role/manage-role.component.html + 18 + + Close subscription info button + + + Export Archive + Archief exporteren + + src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 24 + + Export Archive button + + + Unsubscribe + De-abonneren + + src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 26 + + Unsubscribe button + + + (Paused) + (onderbroken) + + src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 1 + + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 1 + + + src/app/subscriptions/subscriptions.component.html + 12 + + + src/app/subscriptions/subscriptions.component.html + 31 + + + src/app/subscription/subscription/subscription.component.html + 5 + + Paused suffix + + + Archive: + Archief: + + src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.html + 17 + + Subscription ID property + + + Name: + Naam: + + src/app/dialogs/video-info-dialog/video-info-dialog.component.html + 5 + + + src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 6 + + Video name property + + + Uploader: + Uploader: + + src/app/dialogs/video-info-dialog/video-info-dialog.component.html + 13 + + Video ID property + + + File size: + Bestandsgrootte: + + src/app/dialogs/video-info-dialog/video-info-dialog.component.html + 17 + + Video file size property + + + Path: + Pad: + + src/app/dialogs/video-info-dialog/video-info-dialog.component.html + 21 + + Video path property + + + Upload Date: + Uploaddatum: + + src/app/dialogs/video-info-dialog/video-info-dialog.component.html + 25 + + Video upload date property + + + Category: + Categorie: + + src/app/dialogs/video-info-dialog/video-info-dialog.component.html + 29 + + Category property + + + Modify youtube-dl args + youtube-dl-opties aanpassen + + src/app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 1 + + Modify args title + + + Simulated new args + Geteste nieuwe aanvullende opties + + src/app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 8 + + Simulated args title + + + Add an arg + Optie toevoegen + + src/app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 34 + + Add arg card title + + + Search by category + Zoeken op categorie + + src/app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 60 + + Search args by category button + + + Use arg value + Optiewaarde gebruiken + + src/app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 64 + + Use arg value checkbox + + + Add arg + Optie toevoegen + + src/app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 73 + + Search args by category button + + + Modify + Aanpassen + + src/app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 85 + + Arg modifier modify button + + + Arg value + Optiewaarde + + src/app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html + 68 + + Arg value placeholder + + + Updater + Updater + + src/app/dialogs/update-progress-dialog/update-progress-dialog.component.html + 1 + + Update progress dialog title + + + Register a user + Gebruikersregistratie + + src/app/dialogs/add-user-dialog/add-user-dialog.component.html + 1 + + Register user dialog title + + + User name + Gebruikersnaam + + src/app/dialogs/add-user-dialog/add-user-dialog.component.html + 6 + + User name placeholder + + + Register + Registreren + + src/app/dialogs/add-user-dialog/add-user-dialog.component.html + 17 + + + src/app/components/login/login.component.html + 35 + + Register user button + + + Upload new cookies + Nieuwe cookies uploaden + + src/app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html + 1 + + Cookies uploader dialog title + + + NOTE: Uploading new cookies will override your previous cookies. Also note that cookies are instance-wide, not per-user. + Let op: de nieuwe cookies overschrijven de oude. Daarnaast zijn de cookies procesgebonden en niet gebruikersgebonden. + + src/app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html + 20 + + Cookies upload warning + + + Drag and Drop + Slepen-en-neerzetten + + src/app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html + 11 + + Drag and Drop + + + Modify playlist + Afspeellijst aanpassen + + src/app/dialogs/modify-playlist/modify-playlist.component.html + 1 + + Modify playlist dialog title + + + Add content + Inhoud toevoegen + + src/app/dialogs/modify-playlist/modify-playlist.component.html + 19 + + Add content + + + Save + Opslaan + + src/app/dialogs/modify-playlist/modify-playlist.component.html + 37 + + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 68 + + + src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html + 56 + + + src/app/settings/settings.component.html + 416 + + + src/app/components/modify-users/modify-users.component.html + 58 + + Save + + + Normal order + Normale volgorde + + src/app/dialogs/modify-playlist/modify-playlist.component.html + 13 + + Normal order + + + Reverse order + Omgekeerde volgorde + + src/app/dialogs/modify-playlist/modify-playlist.component.html + 14 + + Reverse order + + + My videos + Mijn video's + + src/app/components/recent-videos/recent-videos.component.html + 20 + + My videos title + + + Search + Zoeken + + src/app/components/recent-videos/recent-videos.component.html + 24 + + + src/app/components/modify-users/modify-users.component.html + 7 + + + src/app/subscription/subscription/subscription.component.html + 33 + + Files search placeholder + + + No videos found. + Geen video's gevonden. + + src/app/components/recent-videos/recent-videos.component.html + 38 + + No videos found + + + Editing + Bewerken + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 1 + + Edit subscription dialog title prefix + + + Paused + Onderbroken + + src/app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component.html + 7 + + Paused subscription setting + + + Editing category + Categorie bewerken + + src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html + 1 + + Editing category dialog title + + + Rules + Regels + + src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html + 10 + + Rules + + + Add new rule + Regel toevoegen + + src/app/dialogs/edit-category-dialog/edit-category-dialog.component.html + 39 + + Add new rule tooltip + + + Download Twitch Chat + Twitch-chatgesprek downloaden + + src/app/components/twitch-chat/twitch-chat.component.html + 10 + + Download Twitch Chat button + + + Edit + Aanpassen + + src/app/file-card/file-card.component.html + 19 + + + src/app/components/unified-file-card/unified-file-card.component.html + 37 + + Playlist edit button + + + Delete + Verwijderen + + src/app/file-card/file-card.component.html + 20 + + + src/app/file-card/file-card.component.html + 25 + + + src/app/components/unified-file-card/unified-file-card.component.html + 33 + + + src/app/components/unified-file-card/unified-file-card.component.html + 39 + + Delete playlist + + + Info + Informatie + + src/app/file-card/file-card.component.html + 24 + + + src/app/components/unified-file-card/unified-file-card.component.html + 24 + + + src/app/subscription/subscription-file-card/subscription-file-card.component.html + 7 + + Video info button + + + Count: + Aantal: + + src/app/file-card/file-card.component.html + 8 + + Playlist video count + + + Delete and blacklist + Verwijderen en op zwarte lijst plaatsen + + src/app/file-card/file-card.component.html + 26 + + + src/app/components/unified-file-card/unified-file-card.component.html + 34 + + Delete and blacklist video button + + + views + weergaven + + src/app/player/player.component.html + 15 + + View count label + + + Save changes + Aanpassingen opslaan + + src/app/player/player.component.html + 59 + + Playlist save changes button + + + The download was successful + Het downloaden is voltooid + + src/app/download-item/download-item.component.html + 8 + + + src/app/download-item/download-item.component.html + 8 + + download successful tooltip + + + An error has occurred + Er is een fout opgetreden + + src/app/download-item/download-item.component.html + 9 + + + src/app/download-item/download-item.component.html + 9 + + download error tooltip + + + Details + Details + + src/app/download-item/download-item.component.html + 18 + + Details + + + An error has occurred: + Er is een fout opgetreden: + + src/app/download-item/download-item.component.html + 27 + + Error label + + + Download start: + Gestart om: + + src/app/download-item/download-item.component.html + 32 + + Download start label + + + Download end: + Afgerond om: + + src/app/download-item/download-item.component.html + 35 + + Download end label + + + File path(s): + Bestandspad(en): + + src/app/download-item/download-item.component.html + 38 + + File path(s) label + + + Your subscriptions + Mijn abonnementen + + src/app/subscriptions/subscriptions.component.html + 3 + + Subscriptions title + + + Channels + Kanalen + + src/app/subscriptions/subscriptions.component.html + 8 + + Subscriptions channels title + + + Playlists + Afspeellijsten + + src/app/subscriptions/subscriptions.component.html + 27 + + Subscriptions playlists title + + + Name not available. Channel retrieval in progress. + De naam is niet beschikbaar omdat het kanaal nog wordt opgehaald. + + src/app/subscriptions/subscriptions.component.html + 14 + + Subscription playlist not available text + + + You have no channel subscriptions. + Je hebt geen abonnementen. + + src/app/subscriptions/subscriptions.component.html + 24 + + No channel subscriptions text + + + Name not available. Playlist retrieval in progress. + De naam is niet beschikbaar omdat de afspeellijst nog wordt opgehaald. + + src/app/subscriptions/subscriptions.component.html + 33 + + Subscription playlist not available text + + + You have no playlist subscriptions. + Je hebt geen abonnementen. + + src/app/subscriptions/subscriptions.component.html + 43 + + No playlist subscriptions text + + + Main + Algemeen + + src/app/settings/settings.component.html + 12 + + Main settings label + + + Downloader + Downloader + + src/app/settings/settings.component.html + 102 + + Downloader settings label + + + Extra + Diversen + + src/app/settings/settings.component.html + 182 + + Extra settings label + + + Advanced + Geavanceerd + + src/app/settings/settings.component.html + 281 + + Host settings label + + + Users + Gebruikers + + src/app/settings/settings.component.html + 355 + + + src/app/settings/settings.component.html + 355 + + Users settings label + + + Logs + Logboeken + + src/app/settings/settings.component.html + 403 + + + src/app/settings/settings.component.html + 403 + + Logs settings label + + + {VAR_SELECT, select, true {Close} false {Cancel} other {otha}} + {VAR_SELECT, select, true {Close} false {Cancel} other {otha}} + + src/app/settings/settings.component.html + 419 + + Settings cancel and close button + + + URL this app will be accessed from, without the port. + De url waarvan deze app wordt geladen, zonder het poortnummer. + + src/app/settings/settings.component.html + 19 + + URL setting input hint + + + Port + Poort + + src/app/settings/settings.component.html + 24 + + Port input placeholder + + + The desired port. Default is 17442. + Het gewenste poortnummer (standaard: 17442). + + src/app/settings/settings.component.html + 25 + + Port setting input hint + + + Multi-user mode + Meerdere gebruikers + + src/app/settings/settings.component.html + 34 + + Multi user mode setting + + + Users base path + Gebruikersbasispad + + src/app/settings/settings.component.html + 38 + + Users base path placeholder + + + Base path for users and their downloaded videos. + Het basispad voor gebruikers en hun gedownloade video's. + + src/app/settings/settings.component.html + 39 + + Users base path hint + + + Allow subscriptions + Abonnementen toestaan + + src/app/settings/settings.component.html + 48 + + Allow subscriptions setting + + + Subscriptions base path + Abonnementenbasispad + + src/app/settings/settings.component.html + 52 + + Subscriptions base path input setting placeholder + + + Base path for videos from your subscribed channels and playlists. It is relative to YTDL-Material's root folder. + Het basispad voor video's van afspeellijsten en kanalen uit je abonnementen. Dit is relatief aan YTDL-Material's hoofdmap. + + src/app/settings/settings.component.html + 53 + + Subscriptions base path setting input hint + + + Check interval + Controletussenpoos + + src/app/settings/settings.component.html + 58 + + Check interval input setting placeholder + + + Unit is seconds, only include numbers. + In seconden (alleen cijfers). + + src/app/settings/settings.component.html + 59 + + Check interval setting input hint + + + Sometimes new videos are downloaded before being fully processed. This setting will mean new videos will be checked for a higher quality version the following day. + Soms worden nieuwe video's gedownload voordat ze volledig verwerkt zijn. Met deze instelling wordt de volgende dag gecontroleerd of er een hogere kwaliteit beschikbaar is. + + src/app/settings/settings.component.html + 63 + + Redownload fresh uploads tooltip + + + Redownload fresh uploads + Nieuwe uploads opnieuw downloaden + + src/app/settings/settings.component.html + 63 + + Redownload fresh uploads + + + Theme + Thema + + src/app/settings/settings.component.html + 72 + + Theme select label + + + Default + Standaard + + src/app/settings/settings.component.html + 74 + + Default theme label + + + Allow theme change + Themawijziging toestaan + + src/app/settings/settings.component.html + 80 + + Allow theme change setting + + + Language + Taal + + src/app/settings/settings.component.html + 89 + + Language select label + + + Audio folder path + Audiopad + + src/app/settings/settings.component.html + 109 + + Audio folder path input placeholder + + + Path for audio only downloads. It is relative to YTDL-Material's root folder. + Het pad voor audiodownloads. Dit is relatief aan YTDL-Material's hoofdmap. + + src/app/settings/settings.component.html + 110 + + Aduio path setting input hint + + + Video folder path + Videomap + + src/app/settings/settings.component.html + 116 + + Video folder path input placeholder + + + Path for video downloads. It is relative to YTDL-Material's root folder. + Het pad voor videodownloads. Dit is relatief aan YTDL-Material's hoofdmap. + + src/app/settings/settings.component.html + 117 + + Video path setting input hint + + + Default file output + Standaard bestandsuitvoer + + src/app/settings/settings.component.html + 123 + + Default file output placeholder + + + Path is relative to the above download paths. Don't include extension. + Dit pad is relatief aan bovenstaande downloadpaden. Laat de extensie achterwege. + + src/app/settings/settings.component.html + 126 + + Custom Output input hint + + + Global custom args + Algemene aanvullende opties + + src/app/settings/settings.component.html + 133 + + Custom args input placeholder + + + Global custom args for downloads on the home page. Args are delimited using two commas like so: ,, + Algemene aanvullende opties voor downloads op de overzichtspagina. Scheidt deze met komma's: ,, + + src/app/settings/settings.component.html + 134 + + Custom args setting input hint + + + Categories + Categorieën + + src/app/settings/settings.component.html + 144 + + Categories + + + Use youtube-dl archive + youtube-dl-archief gebruiken + + src/app/settings/settings.component.html + 163 + + Use youtubedl archive setting + + + Include thumbnail + Miniatuurvoorbeeld opslaan + + src/app/settings/settings.component.html + 167 + + Include thumbnail setting + + + Include metadata + Metagegevens opslaan + + src/app/settings/settings.component.html + 171 + + Include metadata setting + + + Kill all downloads + Alle downloads afbreken + + src/app/settings/settings.component.html + 175 + + Kill all downloads button + + + Top title + Boventitel + + src/app/settings/settings.component.html + 188 + + Top title input placeholder + + + File manager enabled + Bestandsbeheer ingeschakeld + + src/app/settings/settings.component.html + 193 + + File manager enabled setting + + + Downloads manager enabled + Downloadbeheer ingeschakeld + + src/app/settings/settings.component.html + 196 + + Downloads manager enabled setting + + + Allow quality select + Kwaliteitskeuze toestaan + + src/app/settings/settings.component.html + 199 + + Allow quality seelct setting + + + Download only mode + Downloadmodus + + src/app/settings/settings.component.html + 202 + + Download only mode setting + + + Allow multi-download mode + Meerdere downloads toestaan + + src/app/settings/settings.component.html + 205 + + Allow multi-download mode setting + + + Enable Public API + Openbare api gebruiken + + src/app/settings/settings.component.html + 213 + + Enable Public API key setting + + + Public API Key + Openbare api-sleutel + + src/app/settings/settings.component.html + 218 + + Public API Key setting placeholder + + + View documentation + Documentatie bekijken + + src/app/settings/settings.component.html + 219 + + View API docs setting hint + + + This will delete your old API key! + Let op: hiermee verwijder je je oude api-sleutel! + + src/app/settings/settings.component.html + 223 + + delete api key tooltip + + + Generate + Genereren + + src/app/settings/settings.component.html + 223 + + Generate key button + + + Use YouTube API + YouTube-api gebruiken + + src/app/settings/settings.component.html + 232 + + Use YouTube API setting + + + Youtube API Key + YouTube-api-sleutel + + src/app/settings/settings.component.html + 236 + + Youtube API Key setting placeholder + + + Generating a key is easy! + Het genereren van een sleutel is eenvoudig. + + src/app/settings/settings.component.html + 237 + + + src/app/settings/settings.component.html + 249 + + Youtube API Key setting hint + + + Use Twitch API + Twitch-api gebruiken + + src/app/settings/settings.component.html + 241 + + Use Twitch API setting + + + Twitch API Key + Twitch-api-sleutel + + src/app/settings/settings.component.html + 248 + + Twitch API Key setting placeholder + + + Also known as a Client ID. + Ook wel de client-id. + + src/app/settings/settings.component.html + 249 + + Twitch API Key setting hint AKA preamble + + + Auto-download Twitch Chat + Twitch-chatgesprekken automatisch downloaden + + src/app/settings/settings.component.html + 244 + + Auto download Twitch Chat setting + + + Click here + Klik hier + + src/app/settings/settings.component.html + 259 + + + src/app/settings/settings.component.html + 265 + + + src/app/dialogs/about-dialog/about-dialog.component.html + 25 + + Chrome ext click here + + + to download the official YoutubeDL-Material Chrome extension manually. + om de officiële Chrome-extensie van YouTubeDL-Material te downloaden. + + src/app/settings/settings.component.html + 259 + + Chrome click here suffix + + + You must manually load the extension and modify the extension's settings to set the frontend URL. + Hiervoor dien je de extensie handmatig te laden en de frontend-url op te geven in de instellingen. + + src/app/settings/settings.component.html + 260 + + Chrome setup suffix + + + to install the official YoutubeDL-Material Firefox extension right off the Firefox extensions page. + om de officiële Firefox-extensie van YouTubeDL-Material te installeren. + + src/app/settings/settings.component.html + 265 + + Firefox click here suffix + + + Detailed setup instructions. + Uitgebreide installatiehandleiding. + + src/app/settings/settings.component.html + 266 + + Firefox setup prefix link + + + Not much is required other than changing the extension's settings to set the frontend URL. + Je hoeft alleen de frontend-url op te geven in de instellingen. + + src/app/settings/settings.component.html + 266 + + Firefox setup suffix + + + Drag the link below to your bookmarks, and you're good to go! Just navigate to the YouTube video you'd like to download, and click the bookmark. + Sleep de link naar je bladwijzers en klaar is Kees! Ga vervolgens naar een YouTube-video en klik op de bladwijzer. + + src/app/settings/settings.component.html + 271 + + Bookmarklet instructions + + + Generate 'audio only' bookmarklet + Audio-bookmarklet genereren + + src/app/settings/settings.component.html + 272 + + Generate audio only bookmarklet checkbox + + + Select a downloader + Kies een downloader + + src/app/settings/settings.component.html + 287 + + Default downloader select label + + + Use default downloading agent + Standaard downloadagent gebruiken + + src/app/settings/settings.component.html + 295 + + Use default downloading agent setting + + + Select a download agent + Kies een downloadagent + + src/app/settings/settings.component.html + 299 + + Custom downloader select label + + + Log Level + Logniveau + + src/app/settings/settings.component.html + 313 + + Log Level label + + + Login expiration + Inlogverloopdatum + + src/app/settings/settings.component.html + 325 + + Login expiration select label + + + Allow advanced download + Geavanceerd downloaden toestaan + + src/app/settings/settings.component.html + 336 + + Allow advanced downloading setting + + + Use Cookies + Cookies gebruiken + + src/app/settings/settings.component.html + 344 + + Use cookies setting + + + Set Cookies + Cookies instellen + + src/app/settings/settings.component.html + 345 + + Set cookies button + + + Allow user registration + Gebruikersregistratie toestaan + + src/app/settings/settings.component.html + 359 + + Allow registration setting + + + Auth method + Authenticatiemethode + + src/app/settings/settings.component.html + 363 + + Auth method select + + + Internal + Intern + + src/app/settings/settings.component.html + 365 + + Internal auth method + + + LDAP + LDAP + + src/app/settings/settings.component.html + 368 + + LDAP auth method + + + LDAP URL + LDAP-url + + src/app/settings/settings.component.html + 375 + + LDAP URL + + + Bind DN + Bind DN + + src/app/settings/settings.component.html + 380 + + Bind DN + + + Bind Credentials + Bind-inloggegevens + + src/app/settings/settings.component.html + 385 + + Bind Credentials + + + Search Base + Zoekdatabank + + src/app/settings/settings.component.html + 390 + + Search Base + + + Search Filter + Zoekfilter + + src/app/settings/settings.component.html + 395 + + Search Filter + + + About YoutubeDL-Material + Over YouTubeDL-Material + + src/app/dialogs/about-dialog/about-dialog.component.html + 1 + + About dialog title + + + is an open-source YouTube downloader built under Google's Material Design specifications. You can seamlessly download your favorite videos as video or audio files, and even subscribe to your favorite channels and playlists to keep updated with their new videos. + is een opensource YouTube-downloader, gebouwd volgens Google's Material Design-specificaties. Je kunt naadloos je favoriete video's downloaden als audio- of videobestanden of abonneren op je favoriete kanalen of afspeellijsten om altijd de nieuwste video's binnen te halen. + + src/app/dialogs/about-dialog/about-dialog.component.html + 12 + + About first paragraph + + + has some awesome features included! An extensive API, Docker support, and localization (translation) support. Read up on all the supported features by clicking on the GitHub icon above. + bevat een aantal handige functies, zoals een uitgebreide api, Docker-ondersteuning en is volledig vertaalbaar. Meer functies zijn te vinden op onze GitHub-pagina (klik op het GitHub-pictogram). + + src/app/dialogs/about-dialog/about-dialog.component.html + 15 + + About second paragraph + + + Installed version: + Geïnstalleerde versie: + + src/app/dialogs/about-dialog/about-dialog.component.html + 20 + + Version label + + + Found a bug or have a suggestion? + Heb je een bug aangetroffen of een idee? + + src/app/dialogs/about-dialog/about-dialog.component.html + 25 + + About bug prefix + + + to create an issue! + om een 'issue' te openen! + + src/app/dialogs/about-dialog/about-dialog.component.html + 25 + + About bug suffix + + + Checking for updates... + Bezig met controleren op updates... + + src/app/dialogs/about-dialog/about-dialog.component.html + 20 + + Checking for updates text + + + Update available + Update beschikbaar + + src/app/dialogs/about-dialog/about-dialog.component.html + 21 + + View latest update + + + You can update from the settings menu. + Je kunt de update installeren via het instellingenmenu. + + src/app/dialogs/about-dialog/about-dialog.component.html + 21 + + Update through settings menu hint + + + Select a version: + Kies een versie: + + src/app/updater/updater.component.html + 3 + + Select a version + + + Enable sharing + Delen toestaan + + src/app/dialogs/share-media-dialog/share-media-dialog.component.html + 10 + + Enable sharing checkbox + + + Use timestamp + Tijdstempel gebruiken + + src/app/dialogs/share-media-dialog/share-media-dialog.component.html + 13 + + Use timestamp + + + Seconds + seconden + + src/app/dialogs/share-media-dialog/share-media-dialog.component.html + 15 + + Seconds + + + Copy to clipboard + Kopiëren naar klembord + + src/app/dialogs/share-media-dialog/share-media-dialog.component.html + 24 + + Copy to clipboard button + + + Share playlist + Afspeellijst delen + + src/app/dialogs/share-media-dialog/share-media-dialog.component.html + 2 + + Share playlist dialog title + + + Share video + Video delen + + src/app/dialogs/share-media-dialog/share-media-dialog.component.html + 3 + + Share video dialog title + + + Share audio + Audio delen + + src/app/dialogs/share-media-dialog/share-media-dialog.component.html + 4 + + Share audio dialog title + + + Session ID: + Sessie-id: + + src/app/components/downloads/downloads.component.html + 5 + + Session ID + + + Clear all downloads + Alle downloads wissen + + src/app/components/downloads/downloads.component.html + 18 + + clear all downloads action button + + + (current) + (huidig) + + src/app/components/downloads/downloads.component.html + 6 + + Current session + + + No downloads available! + Geen downloads beschikbaar! + + src/app/components/downloads/downloads.component.html + 25 + + No downloads label + + + Your Profile + Mijn profiel + + src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 1 + + User profile dialog title + + + Logout + Uitloggen + + src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 28 + + Logout + + + UID: + UID: + + src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 9 + + UID + + + Created: + Aangemaakt: + + src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 12 + + Created + + + You are not logged in. + Je bent niet ingelogd. + + src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html + 19 + + Not logged in notification + + + Create admin account + Beheerdersaccount aanmaken + + src/app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html + 1 + + Create admin account dialog title + + + No default admin account detected. This will create and set the password for an admin account with the user name as 'admin'. + Er zijn geen beheerdersaccounts aangetroffen. Hiermee maak je een beheerdersaccount met wachtwoord aan - de gebruikersnaam is 'admin'. + + src/app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html + 5 + + No default admin detected explanation + + + Create + Aanmaken + + src/app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html + 17 + + Create + + + Add Users + Gebruikers toevoegen + + src/app/components/modify-users/modify-users.component.html + 90 + + Add users button + + + Edit Role + Rol aanpassen + + src/app/components/modify-users/modify-users.component.html + 95 + + Edit role + + + User name + Gebruikersnaam + + src/app/components/modify-users/modify-users.component.html + 17 + + Username users table header + + + Role + Rol + + src/app/components/modify-users/modify-users.component.html + 35 + + Role users table header + + + Actions + Acties + + src/app/components/modify-users/modify-users.component.html + 55 + + Actions users table header + + + Manage user + Gebruiker beheren + + src/app/components/modify-users/modify-users.component.html + 70 + + + src/app/components/manage-user/manage-user.component.html + 1 + + manage user action button tooltip + + + Delete user + Gebruiker verwijderen + + src/app/components/modify-users/modify-users.component.html + 73 + + delete user action button tooltip + + + Edit user + Gebruiker bewerken + + src/app/components/modify-users/modify-users.component.html + 66 + + edit user action button tooltip + + + User UID: + Gebruikers-uid: + + src/app/components/manage-user/manage-user.component.html + 4 + + User UID + + + New password + Nieuw wachtwoord + + src/app/components/manage-user/manage-user.component.html + 8 + + New password placeholder + + + Set new password + Nieuw wachtwoord instellen + + src/app/components/manage-user/manage-user.component.html + 10 + + Set new password + + + Use role default + Standaardrol gebruiken + + src/app/components/manage-user/manage-user.component.html + 19 + + Use role default + + + Yes + Ja + + src/app/components/manage-user/manage-user.component.html + 20 + + + src/app/components/manage-role/manage-role.component.html + 9 + + Yes + + + No + Nee + + src/app/components/manage-user/manage-user.component.html + 21 + + + src/app/components/manage-role/manage-role.component.html + 10 + + No + + + Manage role + Rol beheren + + src/app/components/manage-role/manage-role.component.html + 1 + + Manage role dialog title + + + Lines: + Aantal regels: + + src/app/components/logs-viewer/logs-viewer.component.html + 22 + + Label for lines select in logger view + + + Clear logs + Logboeken wissen + + src/app/components/logs-viewer/logs-viewer.component.html + 34 + + Clear logs button + + + Auto-generated + Automatisch gegenereerd + + src/app/components/unified-file-card/unified-file-card.component.html + 5 + + Auto-generated label + + + Open file + Bestand openen + + src/app/components/unified-file-card/unified-file-card.component.html + 18 + + Open file button + + + Open file in new tab + Bestand openen op nieuw tabblad + + src/app/components/unified-file-card/unified-file-card.component.html + 19 + + Open file in new tab + + + Go to subscription + Ga naar abonnement + + src/app/components/unified-file-card/unified-file-card.component.html + 25 + + Go to subscription menu item + + + Delete and redownload + Verwijderen en opnieuw downloaden + + src/app/components/unified-file-card/unified-file-card.component.html + 28 + + + src/app/subscription/subscription-file-card/subscription-file-card.component.html + 8 + + Delete and redownload subscription video button + + + Delete forever + Permanent verwijderen + + src/app/components/unified-file-card/unified-file-card.component.html + 31 + + + src/app/subscription/subscription-file-card/subscription-file-card.component.html + 9 + + Delete forever subscription video button + + + See more. + Meer tonen. + + src/app/components/see-more/see-more.component.html + 5,6 + + See more + + + See less. + Minder tonen. + + src/app/components/see-more/see-more.component.html + 8,9 + + See less + + + Length: + Duur: + + src/app/subscription/subscription-file-card/subscription-file-card.component.html + 3 + + Video duration label + + + + From 433d08e9dfdb641c111107408f8fba71fb5475ad Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Fri, 12 Feb 2021 21:20:48 -0700 Subject: [PATCH 034/680] Added ability to crop files Fixed bug in downloading playlists --- backend/app.js | 44 ++++++++++++++++++++++++++++---- backend/db.js | 7 ++++- src/app/main/main.component.css | 4 +++ src/app/main/main.component.html | 21 +++++++++++++-- src/app/main/main.component.ts | 14 +++++++++- src/app/posts.services.ts | 5 ++-- 6 files changed, 84 insertions(+), 11 deletions(-) diff --git a/backend/app.js b/backend/app.js index 9dbad30..dba7f14 100644 --- a/backend/app.js +++ b/backend/app.js @@ -853,7 +853,7 @@ async function createPlaylistZipFile(fileNames, type, outputName, fullPathProvid let zipFolderPath = null; if (!fullPathProvided) { - zipFolderPath = path.join(__dirname, (type === 'audio') ? audioFolderPath : videoFolderPath); + zipFolderPath = (type === 'audio') ? audioFolderPath : videoFolderPath if (user_uid) zipFolderPath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, zipFolderPath); } else { zipFolderPath = path.join(__dirname, config_api.getConfigItem('ytdl_subscriptions_base_path')); @@ -1155,7 +1155,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) { } // download file - youtubedl.exec(url, downloadConfig, {}, function(err, output) { + youtubedl.exec(url, downloadConfig, {}, async function(err, output) { if (download_checker) clearInterval(download_checker); // stops the download checker from running as the download finished (or errored) download['downloading'] = false; @@ -1227,8 +1227,12 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) { const file_path = options.noRelativePath ? path.basename(full_file_path) : full_file_path.substring(fileFolderPath.length, full_file_path.length); const customPath = options.noRelativePath ? path.dirname(full_file_path).split(path.sep).pop() : null; + if (options.cropFileSettings) { + await cropFile(full_file_path, options.cropFileSettings.cropFileStart, options.cropFileSettings.cropFileEnd, ext); + } + // registers file in DB - file_uid = db_api.registerFileDB(file_path, type, multiUserMode, null, customPath, category); + file_uid = db_api.registerFileDB(file_path, type, multiUserMode, null, customPath, category, options.cropFileSettings); if (file_name) file_names.push(file_name); } @@ -1587,6 +1591,8 @@ async function getUrlInfos(urls) { }); } +// ffmpeg helper functions + async function convertFileToMp3(input_file, output_file) { logger.verbose(`Converting ${input_file} to ${output_file}...`); return new Promise(resolve => { @@ -1604,6 +1610,33 @@ async function convertFileToMp3(input_file, output_file) { }); } +async function cropFile(file_path, start, end, ext) { + return new Promise(resolve => { + const temp_file_path = `${file_path}.cropped${ext}`; + let base_ffmpeg_call = ffmpeg(file_path); + if (start) { + base_ffmpeg_call = base_ffmpeg_call.seekOutput(start); + } + if (end) { + base_ffmpeg_call = base_ffmpeg_call.duration(end - start); + } + base_ffmpeg_call + .on('end', () => { + logger.verbose(`Cropping for '${file_path}' complete.`); + fs.unlinkSync(file_path); + fs.moveSync(temp_file_path, file_path); + resolve(true); + }) + .on('error', (err, test, test2) => { + logger.error(`Failed to crop ${file_path}.`); + logger.error(err); + resolve(false); + }).save(temp_file_path); + }); +} + +// archive helper functions + async function writeToBlacklist(type, line) { let blacklistPath = path.join(archivePath, (type === 'audio') ? 'blacklist_audio.txt' : 'blacklist_video.txt'); // adds newline to the beginning of the line @@ -1908,7 +1941,8 @@ app.post('/api/tomp4', optionalJwt, async function(req, res) { youtubeUsername: req.body.youtubeUsername, youtubePassword: req.body.youtubePassword, ui_uid: req.body.ui_uid, - user: req.isAuthenticated() ? req.user.uid : null + user: req.isAuthenticated() ? req.user.uid : null, + cropFileSettings: req.body.cropFileSettings } const safeDownloadOverride = config_api.getConfigItem('ytdl_safe_download_override') || config_api.globalArgsRequiresSafeDownload(); @@ -2666,7 +2700,7 @@ app.post('/api/downloadFile', optionalJwt, async (req, res) => { for (let i = 0; i < fileNames.length; i++) { fileNames[i] = decodeURIComponent(fileNames[i]); } - file = await createPlaylistZipFile(fileNames, type, outputName, fullPathProvided, req.body.uuid); + file = await createPlaylistZipFile(fileNames, type, outputName, fullPathProvided, req.body.uuid || req.user.uid); if (!path.isAbsolute(file)) file = path.join(__dirname, file); } res.sendFile(file, function (err) { diff --git a/backend/db.js b/backend/db.js index 6d5cdb6..16a8d0c 100644 --- a/backend/db.js +++ b/backend/db.js @@ -15,7 +15,7 @@ function initialize(input_db, input_users_db, input_logger) { setLogger(input_logger); } -function registerFileDB(file_path, type, multiUserMode = null, sub = null, customPath = null, category = null) { +function registerFileDB(file_path, type, multiUserMode = null, sub = null, customPath = null, category = null, cropFileSettings = null) { let db_path = null; const file_id = utils.removeFileExtension(file_path); const file_object = generateFileObject(file_id, type, customPath || multiUserMode && multiUserMode.file_path, sub); @@ -32,6 +32,11 @@ function registerFileDB(file_path, type, multiUserMode = null, sub = null, custo // if category exists, only include essential info if (category) file_object['category'] = {name: category['name'], uid: category['uid']}; + // modify duration + if (cropFileSettings) { + file_object['duration'] = (cropFileSettings.cropFileEnd || file_object.duration) - cropFileSettings.cropFileStart; + } + if (!sub) { if (multiUserMode) { const user_uid = multiUserMode.user; diff --git a/src/app/main/main.component.css b/src/app/main/main.component.css index 4ee7412..8411370 100644 --- a/src/app/main/main.component.css +++ b/src/app/main/main.component.css @@ -124,6 +124,10 @@ mat-form-field.mat-form-field { width: 100%; } +.advanced-input-time { + margin-left: 10px; +} + .edit-button { margin-left: 10px; top: -5px; diff --git a/src/app/main/main.component.html b/src/app/main/main.component.html index 9cb905e..26b9da3 100644 --- a/src/app/main/main.component.html +++ b/src/app/main/main.component.html @@ -129,7 +129,7 @@ -
+
Use authentication @@ -139,11 +139,28 @@
-
+
+
+ + + Crop file + + + + + Seconds + +
+
+ + + Seconds + +
diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts index 30694bf..51a90ce 100644 --- a/src/app/main/main.component.ts +++ b/src/app/main/main.component.ts @@ -54,6 +54,9 @@ export class MainComponent implements OnInit { youtubeAuthEnabled = false; youtubeUsername = null; youtubePassword = null; + cropFile = false; + cropFileStart = null; + cropFileEnd = null; urlError = false; path = ''; url = ''; @@ -521,8 +524,17 @@ export class MainComponent implements OnInit { const customQualityConfiguration = this.getSelectedVideoFormat(); + let cropFileSettings = null; + + if (this.cropFile) { + cropFileSettings = { + cropFileStart: this.cropFileStart, + cropFileEnd: this.cropFileEnd + } + } + this.postsService.makeMP4(this.url, (this.selectedQuality === '' ? null : this.selectedQuality), - customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword, new_download.uid).subscribe(posts => { + customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword, new_download.uid, cropFileSettings).subscribe(posts => { // update download object new_download.downloading = false; new_download.percent_complete = 100; diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 6657f00..a6c2954 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -183,7 +183,7 @@ export class PostsService implements CanActivate { } // tslint:disable-next-line: max-line-length - makeMP4(url: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, ui_uid = null) { + makeMP4(url: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, ui_uid = null, cropFileSettings = null) { return this.http.post(this.path + 'tomp4', {url: url, selectedHeight: selectedQuality, customQualityConfiguration: customQualityConfiguration, @@ -191,7 +191,8 @@ export class PostsService implements CanActivate { customOutput: customOutput, youtubeUsername: youtubeUsername, youtubePassword: youtubePassword, - ui_uid: ui_uid}, this.httpOptions); + ui_uid: ui_uid, + cropFileSettings: cropFileSettings}, this.httpOptions); } killAllDownloads() { From 023df9c29d673234d10796a80f4b099f86caaac5 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Fri, 12 Feb 2021 21:21:09 -0700 Subject: [PATCH 035/680] Fixed issue where playlists couldn't be favorited after downloading --- src/app/player/player.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/player/player.component.html b/src/app/player/player.component.html index 086adf6..7f8d928 100644 --- a/src/app/player/player.component.html +++ b/src/app/player/player.component.html @@ -8,7 +8,7 @@ -
+
@@ -29,7 +29,7 @@
- + From 669c87dd1bb375c586c26db5a00222cd6e73bbb7 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Fri, 12 Feb 2021 21:21:45 -0700 Subject: [PATCH 036/680] Removed unecessary suffix in crop file inputs --- src/app/main/main.component.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/main/main.component.html b/src/app/main/main.component.html index 26b9da3..f3a1090 100644 --- a/src/app/main/main.component.html +++ b/src/app/main/main.component.html @@ -152,13 +152,11 @@ - Seconds
- Seconds
From c660c284229be2c6f0132aa9c2ca8439d6871a17 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Mon, 22 Feb 2021 12:53:21 -0700 Subject: [PATCH 037/680] youtube-dl now updates in the same way as the other forks --- backend/app.js | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/backend/app.js b/backend/app.js index 209906e..e639ff1 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1681,9 +1681,8 @@ async function autoUpdateYoutubeDL() { let current_app_details_path = 'node_modules/youtube-dl/bin/details'; let current_app_details_exists = fs.existsSync(current_app_details_path); if (!current_app_details_exists) { - logger.error(`Failed to get youtube-dl binary details at location '${current_app_details_path}'. Cancelling update check.`); - resolve(false); - return; + logger.warn(`Failed to get youtube-dl binary details at location '${current_app_details_path}'. Generating file...`); + fs.writeJSONSync('node_modules/youtube-dl/bin/details', {"version":"2020.00.00", "downloader": default_downloader}); } let current_app_details = JSON.parse(fs.readFileSync(current_app_details_path)); let current_version = current_app_details['version']; @@ -1739,20 +1738,15 @@ async function autoUpdateYoutubeDL() { }); } -async function downloadLatestYoutubeDLBinary() { - return new Promise(resolve => { - let binary_path = 'node_modules/youtube-dl/bin'; - downloader(binary_path, function error(err, done) { - if (err) { - logger.error(`youtube-dl failed to update. Restart the server to try again.`); - logger.error(err); - resolve(false); - } - logger.info(`youtube-dl successfully updated!`); - updateDetailsJSON(null, 'youtube-dl'); - resolve(true); - }); - }); +async function downloadLatestYoutubeDLBinary(new_version) { + const file_ext = is_windows ? '.exe' : ''; + + const download_url = `https://github.com/ytdl-org/youtube-dl/releases/latest/download/youtube-dl${file_ext}`; + const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`; + + await fetchFile(download_url, output_path, `youtube-dl ${new_version}`); + + updateDetailsJSON(new_version, 'youtube-dl'); } async function downloadLatestYoutubeDLCBinary(new_version) { From 9d09eeffe313a0c872fc78d89bffc57e1800c6bb Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Mon, 22 Feb 2021 12:54:28 -0700 Subject: [PATCH 038/680] Added maxbuffer option to subscriptions --- backend/subscriptions.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/subscriptions.js b/backend/subscriptions.js index b3e2f1e..3a6e0a3 100644 --- a/backend/subscriptions.js +++ b/backend/subscriptions.js @@ -92,7 +92,7 @@ async function getSubscriptionInfo(sub, user_uid = null) { } return new Promise(resolve => { - youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) { + youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, function(err, output) { if (debugMode) { logger.info('Subscribe: got info for subscription ' + sub.id); } @@ -292,7 +292,7 @@ async function getVideosForSub(sub, user_uid = null) { logger.verbose('Subscription: getting videos for subscription ' + sub.name); return new Promise(resolve => { - youtubedl.exec(sub.url, downloadConfig, {}, async function(err, output) { + youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async function(err, output) { updateSubscriptionProperty(sub, {downloading: false}, user_uid); logger.verbose('Subscription: finished check for ' + sub.name); if (err && !output) { @@ -565,7 +565,7 @@ async function checkVideoIfBetterExists(file_obj, sub, user_uid) { const metric_to_compare = sub.type === 'audio' ? 'abr' : 'height'; if (output[metric_to_compare] > file_obj[metric_to_compare]) { // download new video as the simulated one is better - youtubedl.exec(file_obj['url'], downloadConfig, async (err, output) => { + youtubedl.exec(file_obj['url'], downloadConfig, {maxBuffer: Infinity}, async (err, output) => { if (err) { logger.verbose(`Failed to download better version of video ${file_obj['id']}`); } else if (output) { From f32b3947150f577e214d05bd9d67026612b22978 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Mon, 22 Feb 2021 12:55:30 -0700 Subject: [PATCH 039/680] Added maxBuffer option to all downloads --- backend/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/app.js b/backend/app.js index dba7f14..888ccf7 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1155,7 +1155,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) { } // download file - youtubedl.exec(url, downloadConfig, {}, async function(err, output) { + youtubedl.exec(url, downloadConfig, {maxBuffer: Infinity}, async function(err, output) { if (download_checker) clearInterval(download_checker); // stops the download checker from running as the download finished (or errored) download['downloading'] = false; @@ -1569,7 +1569,7 @@ async function getUrlInfos(urls) { let startDate = Date.now(); let result = []; return new Promise(resolve => { - youtubedl.exec(urls.join(' '), ['--dump-json'], {}, (err, output) => { + youtubedl.exec(urls.join(' '), ['--dump-json'], {maxBuffer: Infinity}, (err, output) => { let new_date = Date.now(); let difference = (new_date - startDate)/1000; logger.debug(`URL info retrieval delay: ${difference} seconds.`); From b9f6d290610b409dc781060679686b5858e6fa1b Mon Sep 17 00:00:00 2001 From: controlol <46456214+controlol@users.noreply.github.com> Date: Thu, 4 Mar 2021 12:45:54 +0100 Subject: [PATCH 040/680] escape paths for use with commandline escape qualityPath and fullOutput for use with commandline In order to successfully download files from subscriptions these strings should be escaped to work properly in the commandline. I have seen you use almost the same function (generateArgs()) in app.js. Even though I have never had a problem with this outside subscriptions I would suggest to do the same for that function starting on line 1405 --- backend/subscriptions.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/subscriptions.js b/backend/subscriptions.js index 3a6e0a3..5a0cd5d 100644 --- a/backend/subscriptions.js +++ b/backend/subscriptions.js @@ -366,11 +366,11 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de let appendedBasePath = getAppendedBasePath(sub, basePath); - let fullOutput = `${appendedBasePath}/%(title)s.%(ext)s`; + let fullOutput = `'${appendedBasePath}/%(title)s.%(ext)s'`; if (desired_path) { - fullOutput = `${desired_path}.%(ext)s`; + fullOutput = `'${desired_path}.%(ext)s'`; } else if (sub.custom_output) { - fullOutput = `${appendedBasePath}/${sub.custom_output}.%(ext)s`; + fullOutput = `'${appendedBasePath}/${sub.custom_output}.%(ext)s'`; } let downloadConfig = ['-o', fullOutput, !redownload ? '-ciw' : '-ci', '--write-info-json', '--print-json']; @@ -381,8 +381,8 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de qualityPath.push('-x'); qualityPath.push('--audio-format', 'mp3'); } else { - if (!sub.maxQuality || sub.maxQuality === 'best') qualityPath = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4']; - else qualityPath = ['-f', `bestvideo[height<=${sub.maxQuality}]+bestaudio/best[height<=${sub.maxQuality}]`, '--merge-output-format', 'mp4']; + if (!sub.maxQuality || sub.maxQuality === 'best') qualityPath = ['-f', '\'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4\'']; + else qualityPath = ['-f', `'bestvideo[height<=${sub.maxQuality}]+bestaudio/best[height<=${sub.maxQuality}]'`, '--merge-output-format', 'mp4']; } downloadConfig.push(...qualityPath) From 7e9d1d30dae415c2264ba0a96e30aed49380c872 Mon Sep 17 00:00:00 2001 From: controlol <46456214+controlol@users.noreply.github.com> Date: Thu, 4 Mar 2021 13:46:39 +0100 Subject: [PATCH 041/680] patch qualityPath qualityPath should not be escaped, this results in `could not find format error` --- backend/subscriptions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/subscriptions.js b/backend/subscriptions.js index 5a0cd5d..51b070a 100644 --- a/backend/subscriptions.js +++ b/backend/subscriptions.js @@ -381,8 +381,8 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de qualityPath.push('-x'); qualityPath.push('--audio-format', 'mp3'); } else { - if (!sub.maxQuality || sub.maxQuality === 'best') qualityPath = ['-f', '\'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4\'']; - else qualityPath = ['-f', `'bestvideo[height<=${sub.maxQuality}]+bestaudio/best[height<=${sub.maxQuality}]'`, '--merge-output-format', 'mp4']; + if (!sub.maxQuality || sub.maxQuality === 'best') qualityPath = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4']; + else qualityPath = ['-f', `bestvideo[height<=${sub.maxQuality}]+bestaudio/best[height<=${sub.maxQuality}]`, '--merge-output-format', 'mp4']; } downloadConfig.push(...qualityPath) From 1f0153b17edae9000cb0b0f58d12d082853b8291 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Tue, 16 Mar 2021 20:06:05 -0600 Subject: [PATCH 042/680] Subscription videos being downloaded will get registered into the database as they are added to avoid having to wait until the subscription completes --- backend/db.js | 24 ++++++++++++++++++++++++ backend/subscriptions.js | 13 ++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/backend/db.js b/backend/db.js index 16a8d0c..c8ef8fb 100644 --- a/backend/db.js +++ b/backend/db.js @@ -213,6 +213,29 @@ async function importUnregisteredFiles() { } +async function preimportUnregisteredSubscriptionFile(sub, appendedBasePath) { + const preimported_file_paths = []; + + let dbPath = null; + if (sub.user_uid) + dbPath = users_db.get('users').find({uid: sub.user_uid}).get('subscriptions').find({id: sub.id}).get('videos'); + else + dbPath = db.get('subscriptions').find({id: sub.id}).get('videos'); + + const files = await utils.getDownloadedFilesByType(appendedBasePath, sub.type); + files.forEach(file => { + // check if file exists in db, if not add it + const file_is_registered = !!(dbPath.find({id: file.id}).value()) + if (!file_is_registered) { + // add additional info + registerFileDBManual(dbPath, file); + preimported_file_paths.push(file['path']); + logger.verbose(`Preemptively added subscription file to the database: ${file.id}`); + } + }); + return preimported_file_paths; +} + async function getVideo(file_uid, uuid, sub_id) { const base_db_path = uuid ? users_db.get('users').find({uid: uuid}) : db; const sub_db_path = sub_id ? base_db_path.get('subscriptions').find({id: sub_id}).get('videos') : base_db_path.get('files'); @@ -235,6 +258,7 @@ module.exports = { updatePlaylist: updatePlaylist, getFileDirectoriesAndDBs: getFileDirectoriesAndDBs, importUnregisteredFiles: importUnregisteredFiles, + preimportUnregisteredSubscriptionFile: preimportUnregisteredSubscriptionFile, getVideo: getVideo, setVideoProperty: setVideoProperty } diff --git a/backend/subscriptions.js b/backend/subscriptions.js index 3a6e0a3..7ff1d06 100644 --- a/backend/subscriptions.js +++ b/backend/subscriptions.js @@ -277,6 +277,7 @@ async function getVideosForSub(sub, user_uid = null) { basePath = config_api.getConfigItem('ytdl_subscriptions_base_path'); let appendedBasePath = getAppendedBasePath(sub, basePath); + fs.ensureDirSync(appendedBasePath); let multiUserMode = null; if (user_uid) { @@ -292,8 +293,17 @@ async function getVideosForSub(sub, user_uid = null) { logger.verbose('Subscription: getting videos for subscription ' + sub.name); return new Promise(resolve => { + const preimported_file_paths = []; + const PREIMPORT_INTERVAL = 5000; + const preregister_check = setInterval(() => { + if (sub.streamingOnly) return; + db_api.preimportUnregisteredSubscriptionFile(sub, appendedBasePath); + }, PREIMPORT_INTERVAL); youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async function(err, output) { + // cleanup updateSubscriptionProperty(sub, {downloading: false}, user_uid); + clearInterval(preregister_check); + logger.verbose('Subscription: finished check for ' + sub.name); if (err && !output) { logger.error(err.stderr ? err.stderr : err.message); @@ -337,7 +347,7 @@ async function getVideosForSub(sub, user_uid = null) { } const reset_videos = i === 0; - handleOutputJSON(sub, sub_db, output_json, multiUserMode, reset_videos); + handleOutputJSON(sub, sub_db, output_json, multiUserMode, preimported_file_paths, reset_videos); } if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) { @@ -351,6 +361,7 @@ async function getVideosForSub(sub, user_uid = null) { }, err => { logger.error(err); updateSubscriptionProperty(sub, {downloading: false}, user_uid); + clearInterval(preregister_check); }); } From d11f77a6c9a6c8e420fb99153dbf383be70da58b Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Tue, 16 Mar 2021 22:16:57 -0600 Subject: [PATCH 043/680] Updated yt-dlp paths --- backend/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/app.js b/backend/app.js index e639ff1..29781e7 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1669,7 +1669,7 @@ async function autoUpdateYoutubeDL() { 'func': downloadLatestYoutubeDLCBinary }, 'yt-dlp': { - 'tags_url': 'https://api.github.com/repos/pukkandan/yt-dlp/tags', + 'tags_url': 'https://api.github.com/repos/yt-dlp/yt-dlp/tags', 'func': downloadLatestYoutubeDLPBinary } } @@ -1763,7 +1763,7 @@ async function downloadLatestYoutubeDLCBinary(new_version) { async function downloadLatestYoutubeDLPBinary(new_version) { const file_ext = is_windows ? '.exe' : ''; - const download_url = `https://github.com/pukkandan/yt-dlp/releases/latest/download/youtube-dlc${file_ext}`; + const download_url = `https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp${file_ext}`; const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`; await fetchFile(download_url, output_path, `yt-dlp ${new_version}`); From 4643efbae04a6c00a1a29be2d96cb7e0dc12c836 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Tue, 16 Mar 2021 22:41:07 -0600 Subject: [PATCH 044/680] Added ability to restart the server from the frontend Dockerfile/entrypoint.sh now uses nodemon enabling restarting from the UI in a container --- Dockerfile | 2 +- backend/app.js | 21 ++++++++++++++------- backend/entrypoint.sh | 2 +- backend/package.json | 3 ++- src/app/posts.services.ts | 4 ++++ src/app/settings/settings.component.html | 8 ++++++++ src/app/settings/settings.component.ts | 8 ++++++++ 7 files changed, 38 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 75b22d3..477ca3e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,4 +40,4 @@ COPY --chown=$UID:$GID [ "/backend/", "/app/" ] EXPOSE 17442 ENTRYPOINT [ "/app/entrypoint.sh" ] -CMD [ "node", "app.js" ] +CMD [ "npm", "start" ] diff --git a/backend/app.js b/backend/app.js index 888ccf7..f62d86c 100644 --- a/backend/app.js +++ b/backend/app.js @@ -142,15 +142,17 @@ var timestamp_server_start = Date.now(); if (debugMode) logger.info('YTDL-Material in debug mode!'); // check if just updated -const just_restarted = fs.existsSync('restart.json'); -if (just_restarted) { +const just_updated = fs.existsSync('restart_update.json'); +if (just_updated) { updaterStatus = { updating: false, details: 'Update complete! You are now on ' + CONSTS['CURRENT_VERSION'] } - fs.unlinkSync('restart.json'); + fs.unlinkSync('restart_update.json'); } +if (fs.existsSync('restart_general.json')) fs.unlinkSync('restart_general.json'); + // updates & starts youtubedl (commented out b/c of repo takedown) // startYoutubeDL(); @@ -332,7 +334,7 @@ async function startServer() { }); } -async function restartServer() { +async function restartServer(is_update = false) { const restartProcess = () => { spawn('node', ['app.js'], { detached: true, @@ -340,10 +342,11 @@ async function restartServer() { }).unref() process.exit() } - logger.info('Update complete! Restarting server...'); + + logger.info(`${is_update ? 'Update complete! ' : ''}Restarting server...`); // the following line restarts the server through nodemon - fs.writeFileSync('restart.json', 'internal use only'); + fs.writeFileSync(`restart${is_update ? '_update' : '_general'}.json`, 'internal use only'); } async function updateServer(tag) { @@ -386,7 +389,7 @@ async function updateServer(tag) { updating: true, 'details': 'Update complete! Restarting server...' } - restartServer(); + restartServer(true); }, err => { updaterStatus = { updating: false, @@ -1898,7 +1901,11 @@ app.post('/api/setConfig', optionalJwt, function(req, res) { logger.error('Tried to save invalid config file!') res.sendStatus(400); } +}); +app.post('/api/restartServer', optionalJwt, (req, res) => { + restartServer(); + res.send({success: true}); }); app.post('/api/tomp3', optionalJwt, async function(req, res) { diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh index d32ec93..16c37ce 100755 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -CMD="node app.js" +CMD="npm start" # if the first arg starts with "-" pass it to program if [ "${1#-}" != "$1" ]; then diff --git a/backend/package.json b/backend/package.json index 862a09c..496159e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,7 +14,8 @@ "public/*" ], "watch": [ - "restart.json" + "restart_update.json", + "restart_general.json" ] }, "repository": { diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index a6c2954..1fd7929 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -199,6 +199,10 @@ export class PostsService implements CanActivate { return this.http.post(this.path + 'killAllDownloads', {}, this.httpOptions); } + restartServer() { + return this.http.post(this.path + 'restartServer', {}, this.httpOptions); + } + loadNavItems() { if (isDevMode()) { return this.http.get('./assets/default.json'); diff --git a/src/app/settings/settings.component.html b/src/app/settings/settings.component.html index a7d69d6..346797c 100644 --- a/src/app/settings/settings.component.html +++ b/src/app/settings/settings.component.html @@ -353,6 +353,14 @@
+ +
+
+
+ +
+
+
diff --git a/src/app/settings/settings.component.ts b/src/app/settings/settings.component.ts index 2ef36fa..0537103 100644 --- a/src/app/settings/settings.component.ts +++ b/src/app/settings/settings.component.ts @@ -255,6 +255,14 @@ export class SettingsComponent implements OnInit { }); } + restartServer() { + this.postsService.restartServer().subscribe(res => { + this.postsService.openSnackBar('Restarting!'); + }, err => { + this.postsService.openSnackBar('Failed to restart the server.'); + }); + } + // snackbar helper public openSnackBar(message: string, action: string = '') { this.snackBar.open(message, action, { From 4c06bc750ceb386c963c14cfe4d735ca6e1f59fe Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 17 Mar 2021 19:13:52 -0600 Subject: [PATCH 045/680] Fixed issue where on some Docker environments the container failed to start due to the error "nodemon update check failed" --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 477ca3e..db776d4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,8 @@ ENV UID=1000 \ GID=1000 \ USER=youtube +ENV NO_UPDATE_NOTIFIER=true + RUN addgroup -S $USER -g $GID && adduser -D -S $USER -G $USER -u $UID RUN apk add --no-cache \ From 4c1f975eaed45f908096539fe9057b2d7cf522df Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Thu, 18 Mar 2021 19:29:03 -0600 Subject: [PATCH 046/680] Force nodemon to install during the container setup Docker now starts through nodemon directly --- Dockerfile | 4 ++-- backend/entrypoint.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index db776d4..2a36777 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM alpine:3.12 as frontend RUN apk add --no-cache \ npm -RUN npm install -g @angular/cli +RUN npm install -g @angular/cli nodemon WORKDIR /build COPY [ "package.json", "package-lock.json", "/build/" ] @@ -42,4 +42,4 @@ COPY --chown=$UID:$GID [ "/backend/", "/app/" ] EXPOSE 17442 ENTRYPOINT [ "/app/entrypoint.sh" ] -CMD [ "npm", "start" ] +CMD [ "nodemon", "app.js" ] diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh index 16c37ce..d30d4fc 100755 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -CMD="npm start" +CMD="nodemon app.js" # if the first arg starts with "-" pass it to program if [ "${1#-}" != "$1" ]; then From aefdde5401e2a11824406b8ccd48f8fcfebf1a83 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Thu, 18 Mar 2021 20:59:46 -0600 Subject: [PATCH 047/680] Fixed issue (hopefully) where nodemon is not properly installed on Docker --- Dockerfile | 2 +- backend/package.json | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2a36777..7414477 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM alpine:3.12 as frontend RUN apk add --no-cache \ npm -RUN npm install -g @angular/cli nodemon +RUN npm install -g @angular/cli WORKDIR /build COPY [ "package.json", "package-lock.json", "/build/" ] diff --git a/backend/package.json b/backend/package.json index 496159e..2f2e21f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,8 +4,9 @@ "description": "backend for YoutubeDL-Material", "main": "index.js", "scripts": { + "preinstall": "npm i nodemon -g", "test": "echo \"Error: no test specified\" && exit 1", - "start": "nodemon -q app.js" + "start": "nodemon app.js" }, "nodemonConfig": { "ignore": [ @@ -48,7 +49,7 @@ "multer": "^1.4.2", "node-fetch": "^2.6.1", "node-id3": "^0.1.14", - "nodemon": "^2.0.2", + "nodemon": "^2.0.7", "passport": "^0.4.1", "passport-http": "^0.3.0", "passport-jwt": "^4.0.0", From addd54fefd062382b65efe33d9bacb0ee3b80182 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sat, 20 Mar 2021 16:22:59 -0600 Subject: [PATCH 048/680] Switched nodemon to foreverjs to hopefully enable restarting internally and fix runtime errors --- Dockerfile | 3 ++- backend/app.js | 9 +-------- backend/entrypoint.sh | 2 +- backend/package.json | 1 - 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7414477..a17794b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,6 +35,7 @@ RUN apk add --no-cache \ WORKDIR /app COPY --chown=$UID:$GID [ "backend/package.json", "backend/package-lock.json", "/app/" ] +RUN npm install forever -g RUN npm install && chown -R $UID:$GID ./ COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ] @@ -42,4 +43,4 @@ COPY --chown=$UID:$GID [ "/backend/", "/app/" ] EXPOSE 17442 ENTRYPOINT [ "/app/entrypoint.sh" ] -CMD [ "nodemon", "app.js" ] +CMD [ "forever", "app.js" ] diff --git a/backend/app.js b/backend/app.js index f62d86c..c5e2d11 100644 --- a/backend/app.js +++ b/backend/app.js @@ -335,18 +335,11 @@ async function startServer() { } async function restartServer(is_update = false) { - const restartProcess = () => { - spawn('node', ['app.js'], { - detached: true, - stdio: 'inherit' - }).unref() - process.exit() - } - logger.info(`${is_update ? 'Update complete! ' : ''}Restarting server...`); // the following line restarts the server through nodemon fs.writeFileSync(`restart${is_update ? '_update' : '_general'}.json`, 'internal use only'); + process.exit(1); } async function updateServer(tag) { diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh index d30d4fc..611bcc8 100755 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -CMD="nodemon app.js" +CMD="forever app.js" # if the first arg starts with "-" pass it to program if [ "${1#-}" != "$1" ]; then diff --git a/backend/package.json b/backend/package.json index 2f2e21f..a38315d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,7 +4,6 @@ "description": "backend for YoutubeDL-Material", "main": "index.js", "scripts": { - "preinstall": "npm i nodemon -g", "test": "echo \"Error: no test specified\" && exit 1", "start": "nodemon app.js" }, From 4ba471074140f99b07c83fb65b6883b7bfa0a240 Mon Sep 17 00:00:00 2001 From: Ben Ashby Date: Fri, 26 Mar 2021 09:46:20 -0600 Subject: [PATCH 049/680] Added helm chart --- ${HOME}/.helm/starters/sitewards/chart | 1 + chart/.helmignore | 23 ++++ chart/Chart.yaml | 24 ++++ chart/templates/NOTES.txt | 22 +++ chart/templates/_helpers.tpl | 62 +++++++++ chart/templates/appdata-pvc.yaml | 21 +++ chart/templates/audio-pvc.yaml | 21 +++ chart/templates/deployment.yaml | 59 ++++++++ chart/templates/ingress.yaml | 41 ++++++ chart/templates/service.yaml | 15 +++ chart/templates/serviceaccount.yaml | 12 ++ chart/templates/subscriptions-pvc.yaml | 21 +++ chart/templates/tests/test-connection.yaml | 15 +++ chart/templates/users-pvc.yaml | 21 +++ chart/templates/video-pvc.yaml | 21 +++ chart/values.yaml | 148 +++++++++++++++++++++ 16 files changed, 527 insertions(+) create mode 160000 ${HOME}/.helm/starters/sitewards/chart create mode 100644 chart/.helmignore create mode 100644 chart/Chart.yaml create mode 100644 chart/templates/NOTES.txt create mode 100644 chart/templates/_helpers.tpl create mode 100644 chart/templates/appdata-pvc.yaml create mode 100644 chart/templates/audio-pvc.yaml create mode 100644 chart/templates/deployment.yaml create mode 100644 chart/templates/ingress.yaml create mode 100644 chart/templates/service.yaml create mode 100644 chart/templates/serviceaccount.yaml create mode 100644 chart/templates/subscriptions-pvc.yaml create mode 100644 chart/templates/tests/test-connection.yaml create mode 100644 chart/templates/users-pvc.yaml create mode 100644 chart/templates/video-pvc.yaml create mode 100644 chart/values.yaml diff --git a/${HOME}/.helm/starters/sitewards/chart b/${HOME}/.helm/starters/sitewards/chart new file mode 160000 index 0000000..bde4ce4 --- /dev/null +++ b/${HOME}/.helm/starters/sitewards/chart @@ -0,0 +1 @@ +Subproject commit bde4ce4a00cedaaa88d13fd123a6e2a6b5827427 diff --git a/chart/.helmignore b/chart/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/chart/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/chart/Chart.yaml b/chart/Chart.yaml new file mode 100644 index 0000000..a86aa60 --- /dev/null +++ b/chart/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: youtubedl-material +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "4.2" diff --git a/chart/templates/NOTES.txt b/chart/templates/NOTES.txt new file mode 100644 index 0000000..bf07841 --- /dev/null +++ b/chart/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "youtubedl-material.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "youtubedl-material.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "youtubedl-material.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "youtubedl-material.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/chart/templates/_helpers.tpl b/chart/templates/_helpers.tpl new file mode 100644 index 0000000..883b89e --- /dev/null +++ b/chart/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "youtubedl-material.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "youtubedl-material.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "youtubedl-material.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "youtubedl-material.labels" -}} +helm.sh/chart: {{ include "youtubedl-material.chart" . }} +{{ include "youtubedl-material.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "youtubedl-material.selectorLabels" -}} +app.kubernetes.io/name: {{ include "youtubedl-material.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "youtubedl-material.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "youtubedl-material.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/chart/templates/appdata-pvc.yaml b/chart/templates/appdata-pvc.yaml new file mode 100644 index 0000000..0cd5e0e --- /dev/null +++ b/chart/templates/appdata-pvc.yaml @@ -0,0 +1,21 @@ +{{- if and .Values.persistence.appData.enabled (not .Values.persistence.appData.existingClaim) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: {{ template "youtubedl-material.fullname" . }}-appdata + labels: + {{- include "youtubedl-material.labels" . | nindent 4 }} +spec: + accessModes: + - {{ .Values.persistence.appData.accessMode | quote }} + resources: + requests: + storage: {{ .Values.persistence.appData.size | quote }} + {{- if .Values.persistence.appData.storageClass }} + {{- if (eq "-" .Values.persistence.appData.storageClass) }} + storageClassName: "" + {{- else }} + storageClassName: "{{ .Values.persistence.appData.storageClass }}" + {{- end }} + {{- end }} + {{- end -}} diff --git a/chart/templates/audio-pvc.yaml b/chart/templates/audio-pvc.yaml new file mode 100644 index 0000000..2de3d03 --- /dev/null +++ b/chart/templates/audio-pvc.yaml @@ -0,0 +1,21 @@ +{{- if and .Values.persistence.audio.enabled (not .Values.persistence.audio.existingClaim) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: {{ template "youtubedl-material.fullname" . }}-audio + labels: + {{- include "youtubedl-material.labels" . | nindent 4 }} +spec: + accessModes: + - {{ .Values.persistence.audio.accessMode | quote }} + resources: + requests: + storage: {{ .Values.persistence.audio.size | quote }} + {{- if .Values.persistence.audio.storageClass }} + {{- if (eq "-" .Values.persistence.audio.storageClass) }} + storageClassName: "" + {{- else }} + storageClassName: "{{ .Values.persistence.audio.storageClass }}" + {{- end }} + {{- end }} + {{- end -}} diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml new file mode 100644 index 0000000..02f2fae --- /dev/null +++ b/chart/templates/deployment.yaml @@ -0,0 +1,59 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "youtubedl-material.fullname" . }} + labels: + {{- include "youtubedl-material.labels" . | nindent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + {{- include "youtubedl-material.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "youtubedl-material.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "youtubedl-material.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 17442 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/chart/templates/ingress.yaml b/chart/templates/ingress.yaml new file mode 100644 index 0000000..79b9ece --- /dev/null +++ b/chart/templates/ingress.yaml @@ -0,0 +1,41 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "youtubedl-material.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "youtubedl-material.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + backend: + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} diff --git a/chart/templates/service.yaml b/chart/templates/service.yaml new file mode 100644 index 0000000..01df5d2 --- /dev/null +++ b/chart/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "youtubedl-material.fullname" . }} + labels: + {{- include "youtubedl-material.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "youtubedl-material.selectorLabels" . | nindent 4 }} diff --git a/chart/templates/serviceaccount.yaml b/chart/templates/serviceaccount.yaml new file mode 100644 index 0000000..e04cc5e --- /dev/null +++ b/chart/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "youtubedl-material.serviceAccountName" . }} + labels: + {{- include "youtubedl-material.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/chart/templates/subscriptions-pvc.yaml b/chart/templates/subscriptions-pvc.yaml new file mode 100644 index 0000000..ad5768c --- /dev/null +++ b/chart/templates/subscriptions-pvc.yaml @@ -0,0 +1,21 @@ +{{- if and .Values.persistence.subscriptions.enabled (not .Values.persistence.subscriptions.existingClaim) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: {{ template "youtubedl-material.fullname" . }}-subscriptions + labels: + {{- include "youtubedl-material.labels" . | nindent 4 }} +spec: + accessModes: + - {{ .Values.persistence.subscriptions.accessMode | quote }} + resources: + requests: + storage: {{ .Values.persistence.subscriptions.size | quote }} + {{- if .Values.persistence.subscriptions.storageClass }} + {{- if (eq "-" .Values.persistence.subscriptions.storageClass) }} + storageClassName: "" + {{- else }} + storageClassName: "{{ .Values.persistence.subscriptions.storageClass }}" + {{- end }} + {{- end }} + {{- end -}} diff --git a/chart/templates/tests/test-connection.yaml b/chart/templates/tests/test-connection.yaml new file mode 100644 index 0000000..3e4b1ba --- /dev/null +++ b/chart/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "youtubedl-material.fullname" . }}-test-connection" + labels: + {{- include "youtubedl-material.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "youtubedl-material.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/chart/templates/users-pvc.yaml b/chart/templates/users-pvc.yaml new file mode 100644 index 0000000..c12c116 --- /dev/null +++ b/chart/templates/users-pvc.yaml @@ -0,0 +1,21 @@ +{{- if and .Values.persistence.users.enabled (not .Values.persistence.users.existingClaim) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: {{ template "youtubedl-material.fullname" . }}-users + labels: + {{- include "youtubedl-material.labels" . | nindent 4 }} +spec: + accessModes: + - {{ .Values.persistence.users.accessMode | quote }} + resources: + requests: + storage: {{ .Values.persistence.users.size | quote }} + {{- if .Values.persistence.users.storageClass }} + {{- if (eq "-" .Values.persistence.users.storageClass) }} + storageClassName: "" + {{- else }} + storageClassName: "{{ .Values.persistence.users.storageClass }}" + {{- end }} + {{- end }} + {{- end -}} diff --git a/chart/templates/video-pvc.yaml b/chart/templates/video-pvc.yaml new file mode 100644 index 0000000..92718ee --- /dev/null +++ b/chart/templates/video-pvc.yaml @@ -0,0 +1,21 @@ +{{- if and .Values.persistence.video.enabled (not .Values.persistence.video.existingClaim) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: {{ template "youtubedl-material.fullname" . }}-video + labels: + {{- include "youtubedl-material.labels" . | nindent 4 }} +spec: + accessModes: + - {{ .Values.persistence.video.accessMode | quote }} + resources: + requests: + storage: {{ .Values.persistence.video.size | quote }} + {{- if .Values.persistence.video.storageClass }} + {{- if (eq "-" .Values.persistence.video.storageClass) }} + storageClassName: "" + {{- else }} + storageClassName: "{{ .Values.persistence.video.storageClass }}" + {{- end }} + {{- end }} + {{- end -}} diff --git a/chart/values.yaml b/chart/values.yaml new file mode 100644 index 0000000..fcb8b99 --- /dev/null +++ b/chart/values.yaml @@ -0,0 +1,148 @@ +# Default values for youtubedl-material. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: tzahi12345/youtubedl-material + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 17442 + +ingress: + enabled: false + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: [] + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +persistence: + appData: + enabled: true + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + ## If you want to reuse an existing claim, you can pass the name of the PVC using + ## the existingClaim variable + # existingClaim: your-claim + accessMode: ReadWriteOnce + size: 1Gi + audio: + enabled: true + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + ## + ## If you want to reuse an existing claim, you can pass the name of the PVC using + ## the existingClaim variable + # existingClaim: your-claim + accessMode: ReadWriteOnce + size: 50Gi + video: + enabled: true + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + ## + ## If you want to reuse an existing claim, you can pass the name of the PVC using + ## the existingClaim variable + # existingClaim: your-claim + accessMode: ReadWriteOnce + size: 50Gi + subscriptions: + enabled: true + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + ## + ## If you want to reuse an existing claim, you can pass the name of the PVC using + ## the existingClaim variable + # existingClaim: your-claim + accessMode: ReadWriteOnce + size: 50Gi + users: + enabled: true + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + ## + ## If you want to reuse an existing claim, you can pass the name of the PVC using + ## the existingClaim variable + # existingClaim: your-claim + accessMode: ReadWriteOnce + size: 50Gi + +nodeSelector: {} + +tolerations: [] + +affinity: {} From 59c9237be56101332df6f29915efe1060dddd2d6 Mon Sep 17 00:00:00 2001 From: Ben Ashby Date: Fri, 26 Mar 2021 09:59:02 -0600 Subject: [PATCH 050/680] integrated pvc's --- chart/templates/appdata-pvc.yaml | 12 +++---- chart/templates/deployment.yaml | 62 ++++++++++++++++++++++++++++++++ chart/values.yaml | 7 +++- 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/chart/templates/appdata-pvc.yaml b/chart/templates/appdata-pvc.yaml index 0cd5e0e..e426650 100644 --- a/chart/templates/appdata-pvc.yaml +++ b/chart/templates/appdata-pvc.yaml @@ -1,4 +1,4 @@ -{{- if and .Values.persistence.appData.enabled (not .Values.persistence.appData.existingClaim) }} +{{- if and .Values.persistence.appdata.enabled (not .Values.persistence.appdata.existingClaim) }} kind: PersistentVolumeClaim apiVersion: v1 metadata: @@ -7,15 +7,15 @@ metadata: {{- include "youtubedl-material.labels" . | nindent 4 }} spec: accessModes: - - {{ .Values.persistence.appData.accessMode | quote }} + - {{ .Values.persistence.appdata.accessMode | quote }} resources: requests: - storage: {{ .Values.persistence.appData.size | quote }} - {{- if .Values.persistence.appData.storageClass }} - {{- if (eq "-" .Values.persistence.appData.storageClass) }} + storage: {{ .Values.persistence.appdata.size | quote }} + {{- if .Values.persistence.appdata.storageClass }} + {{- if (eq "-" .Values.persistence.appdata.storageClass) }} storageClassName: "" {{- else }} - storageClassName: "{{ .Values.persistence.appData.storageClass }}" + storageClassName: "{{ .Values.persistence.appdata.storageClass }}" {{- end }} {{- end }} {{- end -}} diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml index 02f2fae..4d37b75 100644 --- a/chart/templates/deployment.yaml +++ b/chart/templates/deployment.yaml @@ -45,6 +45,68 @@ spec: port: http resources: {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - mountPath: /app/appdata + name: appdata + {{- if .Values.persistence.appdata.subPath }} + subPath: {{ .Values.persistence.appdata.subPath }} + {{- end }} + - mountPath: /app/audio + name: audio + {{- if .Values.persistence.audio.subPath }} + subPath: {{ .Values.persistence.audio.subPath }} + {{- end }} + - mountPath: /app/video + name: video + {{- if .Values.persistence.video.subPath }} + subPath: {{ .Values.persistence.video.subPath }} + {{- end }} + - mountPath: /app/subscriptions + name: subscriptions + {{- if .Values.persistence.subscriptions.subPath }} + subPath: {{ .Values.persistence.subscriptions.subPath }} + {{- end }} + - mountPath: /app/users + name: users + {{- if .Values.persistence.users.subPath }} + subPath: {{ .Values.persistence.users.subPath }} + {{- end }} + volumes: + - name: appdata + {{- if .Values.persistence.appdata.enabled}} + persistentVolumeClaim: + claimName: {{ if .Values.persistence.appdata.existingClaim }}{{ .Values.persistence.appdata.existingClaim }}{{- else }}{{ template "youtubedl-material.fullname" . }}-appdata{{- end }} + {{- else }} + emptyDir: {} + {{- end }} + - name: audio + {{- if .Values.persistence.audio.enabled}} + persistentVolumeClaim: + claimName: {{ if .Values.persistence.audio.existingClaim }}{{ .Values.persistence.audio.existingClaim }}{{- else }}{{ template "youtubedl-material.fullname" . }}-audio{{- end }} + {{- else }} + emptyDir: {} + {{- end }} + - name: subscriptions + {{- if .Values.persistence.subscriptions.enabled}} + persistentVolumeClaim: + claimName: {{ if .Values.persistence.subscriptions.existingClaim }}{{ .Values.persistence.subscriptions.existingClaim }}{{- else }}{{ template "youtubedl-material.fullname" . }}-subscriptions{{- end }} + {{- else }} + emptyDir: {} + {{- end }} + - name: users + {{- if .Values.persistence.users.enabled}} + persistentVolumeClaim: + claimName: {{ if .Values.persistence.users.existingClaim }}{{ .Values.persistence.users.existingClaim }}{{- else }}{{ template "youtubedl-material.fullname" . }}-users{{- end }} + {{- else }} + emptyDir: {} + {{- end }} + - name: video + {{- if .Values.persistence.video.enabled}} + persistentVolumeClaim: + claimName: {{ if .Values.persistence.video.existingClaim }}{{ .Values.persistence.video.existingClaim }}{{- else }}{{ template "youtubedl-material.fullname" . }}-video{{- end }} + {{- else }} + emptyDir: {} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/chart/values.yaml b/chart/values.yaml index fcb8b99..b192e16 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -66,7 +66,7 @@ resources: {} # memory: 128Mi persistence: - appData: + appdata: enabled: true ## If defined, storageClassName: ## If set to "-", storageClassName: "", which disables dynamic provisioning @@ -78,6 +78,7 @@ persistence: ## If you want to reuse an existing claim, you can pass the name of the PVC using ## the existingClaim variable # existingClaim: your-claim + # subPath: some-subpath accessMode: ReadWriteOnce size: 1Gi audio: @@ -93,6 +94,7 @@ persistence: ## If you want to reuse an existing claim, you can pass the name of the PVC using ## the existingClaim variable # existingClaim: your-claim + # subPath: some-subpath accessMode: ReadWriteOnce size: 50Gi video: @@ -108,6 +110,7 @@ persistence: ## If you want to reuse an existing claim, you can pass the name of the PVC using ## the existingClaim variable # existingClaim: your-claim + # subPath: some-subpath accessMode: ReadWriteOnce size: 50Gi subscriptions: @@ -123,6 +126,7 @@ persistence: ## If you want to reuse an existing claim, you can pass the name of the PVC using ## the existingClaim variable # existingClaim: your-claim + # subPath: some-subpath accessMode: ReadWriteOnce size: 50Gi users: @@ -138,6 +142,7 @@ persistence: ## If you want to reuse an existing claim, you can pass the name of the PVC using ## the existingClaim variable # existingClaim: your-claim + # subPath: some-subpath accessMode: ReadWriteOnce size: 50Gi From 4e07440ed2172e54c2ccb068e8f39b81cc07bd45 Mon Sep 17 00:00:00 2001 From: Ben Ashby Date: Sat, 27 Mar 2021 16:34:14 -0600 Subject: [PATCH 051/680] Removed Accidental Dir --- ${HOME}/.helm/starters/sitewards/chart | 1 - 1 file changed, 1 deletion(-) delete mode 160000 ${HOME}/.helm/starters/sitewards/chart diff --git a/${HOME}/.helm/starters/sitewards/chart b/${HOME}/.helm/starters/sitewards/chart deleted file mode 160000 index bde4ce4..0000000 --- a/${HOME}/.helm/starters/sitewards/chart +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bde4ce4a00cedaaa88d13fd123a6e2a6b5827427 From 356a807cad1fc1cdd6530a6ec85cd0af11451c50 Mon Sep 17 00:00:00 2001 From: s55ma Date: Sun, 28 Mar 2021 17:33:47 +0200 Subject: [PATCH 052/680] Update README.md Some packages are missing for Ubuntu/Debian install, especially python. Without python package, you get the following error when trying to download from youtube: 2021-03-28T15:28:30.461Z ERROR: Error while retrieving info on video with URL https://www.youtube.com/watch?v=[some_ID] with the following message: Error: Command failed with exit code 127: /root/youtubedl-material/node_modules/youtube-dl/bin/youtube-dl --dump-json -o video/%(title)s.mp4 --write-info-json --print-json -f bestvideo+bestaudio --merge-output-format mp4 --write-thumbnail http://www.youtube.com/watch?v=[some_ID] 2021-03-28T15:28:30.461Z ERROR: /usr/bin/env: 'python': No such file or directory --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c073161..1c4dcae 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker Debian/Ubuntu: ```bash -sudo apt-get install nodejs youtube-dl ffmpeg +sudo apt-get install nodejs youtube-dl ffmpeg unzip python npm ``` CentOS 7: From 49925848ffb2b46d0c815424118af6ada12d0f64 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 28 Mar 2021 15:51:53 -0400 Subject: [PATCH 053/680] Material Icons are now hosted locally to avoid requesting them from Google for proxied users --- package-lock.json | 7 ++++++- package.json | 1 + src/index.html | 1 - src/styles.scss | 2 ++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f0e3621..7ca348b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "youtube-dl-material", - "version": "4.1.0", + "version": "4.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -7775,6 +7775,11 @@ } } }, + "material-icons": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/material-icons/-/material-icons-0.5.4.tgz", + "integrity": "sha512-5ycazkNmIOtV78Ff3WgvxQESoJuujdRm0cNbf18fmyJN20jHyqp9rpwi4EfQyGimag0ZLElxtVg3H9enIKdOOw==" + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", diff --git a/package.json b/package.json index ab15811..e55369d 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "file-saver": "^2.0.2", "filesize": "^6.1.0", "fingerprintjs2": "^2.1.0", + "material-icons": "^0.5.4", "nan": "^2.14.1", "ng-lazyload-image": "^7.0.1", "ngx-avatar": "^4.0.0", diff --git a/src/index.html b/src/index.html index 69fa759..4bc4bbb 100644 --- a/src/index.html +++ b/src/index.html @@ -10,7 +10,6 @@ - diff --git a/src/styles.scss b/src/styles.scss index 9a776a1..41ef9ea 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,5 +1,7 @@ /* You can add global styles to this file, and also import other style files */ +@import '~material-icons/iconfont/material-icons.css'; + @import '@angular/material/prebuilt-themes/indigo-pink.css'; //@import './app-theme'; From de154a9c3eaf55e037cc42d0fbd72a78afc05584 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 12 May 2021 21:56:21 -0600 Subject: [PATCH 054/680] Updated dockerfile to fix UID/GID bug related to forever.js --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index a17794b..68d648d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,7 @@ ENV UID=1000 \ USER=youtube ENV NO_UPDATE_NOTIFIER=true +ENV FOREVER_ROOT=/app/.forever RUN addgroup -S $USER -g $GID && adduser -D -S $USER -G $USER -u $UID From b3744e616d548738c999c26a9510cb85420873ba Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 12 May 2021 22:52:46 -0600 Subject: [PATCH 055/680] Users can now stream videos concurrently with other users with the new concurrent stream component --- src/app/app.module.ts | 4 +- .../concurrent-stream.component.html | 6 + .../concurrent-stream.component.scss | 7 + .../concurrent-stream.component.spec.ts | 25 ++++ .../concurrent-stream.component.ts | 140 ++++++++++++++++++ 5 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 src/app/components/concurrent-stream/concurrent-stream.component.html create mode 100644 src/app/components/concurrent-stream/concurrent-stream.component.scss create mode 100644 src/app/components/concurrent-stream/concurrent-stream.component.spec.ts create mode 100644 src/app/components/concurrent-stream/concurrent-stream.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0ae868f..ec638bd 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -86,6 +86,7 @@ import { EditCategoryDialogComponent } from './dialogs/edit-category-dialog/edit import { TwitchChatComponent } from './components/twitch-chat/twitch-chat.component'; import { LinkifyPipe, SeeMoreComponent } from './components/see-more/see-more.component'; import { H401Interceptor } from './http.interceptor'; +import { ConcurrentStreamComponent } from './components/concurrent-stream/concurrent-stream.component'; registerLocaleData(es, 'es'); @@ -134,7 +135,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible CustomPlaylistsComponent, EditCategoryDialogComponent, TwitchChatComponent, - SeeMoreComponent + SeeMoreComponent, + ConcurrentStreamComponent ], imports: [ CommonModule, diff --git a/src/app/components/concurrent-stream/concurrent-stream.component.html b/src/app/components/concurrent-stream/concurrent-stream.component.html new file mode 100644 index 0000000..414c4ac --- /dev/null +++ b/src/app/components/concurrent-stream/concurrent-stream.component.html @@ -0,0 +1,6 @@ +
+ + + + +
\ No newline at end of file diff --git a/src/app/components/concurrent-stream/concurrent-stream.component.scss b/src/app/components/concurrent-stream/concurrent-stream.component.scss new file mode 100644 index 0000000..d3b74be --- /dev/null +++ b/src/app/components/concurrent-stream/concurrent-stream.component.scss @@ -0,0 +1,7 @@ +.buttons-container { + display: flex; + align-items: center; + justify-content: center; + margin-top: 15px; + margin-bottom: 15px; +} \ No newline at end of file diff --git a/src/app/components/concurrent-stream/concurrent-stream.component.spec.ts b/src/app/components/concurrent-stream/concurrent-stream.component.spec.ts new file mode 100644 index 0000000..a881ec8 --- /dev/null +++ b/src/app/components/concurrent-stream/concurrent-stream.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConcurrentStreamComponent } from './concurrent-stream.component'; + +describe('ConcurrentStreamComponent', () => { + let component: ConcurrentStreamComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ConcurrentStreamComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ConcurrentStreamComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/concurrent-stream/concurrent-stream.component.ts b/src/app/components/concurrent-stream/concurrent-stream.component.ts new file mode 100644 index 0000000..6c2cc67 --- /dev/null +++ b/src/app/components/concurrent-stream/concurrent-stream.component.ts @@ -0,0 +1,140 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { PostsService } from 'app/posts.services'; + +@Component({ + selector: 'app-concurrent-stream', + templateUrl: './concurrent-stream.component.html', + styleUrls: ['./concurrent-stream.component.scss'] +}) +export class ConcurrentStreamComponent implements OnInit { + + @Input() server_mode = false; + @Input() playback_timestamp; + @Input() playing; + @Input() uid; + + @Output() setPlaybackTimestamp = new EventEmitter(); + @Output() togglePlayback = new EventEmitter(); + @Output() setPlaybackRate = new EventEmitter(); + + started = false; + server_started = false; + watch_together_clicked = false; + + server_already_exists = null; + + check_timeout: any; + update_timeout: any; + + PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_PLAYBACK_MODIFICATION = 0.5; + PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_SKIP = 2; + + PLAYBACK_MODIFIER = 0.1; + + playback_rate_modified = false; + + constructor(private postsService: PostsService) { } + + // flow: click start watching -> check for available stream to enable join button and if user, display "start stream" + // users who join a stream will send continuous requests for info on playback + + ngOnInit(): void { + + } + + startServer() { + this.started = true; + this.server_started = true; + this.update_timeout = setInterval(() => { + this.updateStream(); + }, 1000); + } + + updateStream() { + this.postsService.updateConcurrentStream(this.uid, this.playback_timestamp, Date.now()/1000, this.playing).subscribe(res => { + }); + } + + startClient() { + this.started = true; + } + + checkStream() { + if (this.server_started) { return; } + const current_playback_timestamp = this.playback_timestamp; + const current_unix_timestamp = Date.now()/1000; + this.postsService.checkConcurrentStream(this.uid).subscribe(res => { + const stream = res['stream']; + + if (!stream) { + this.server_already_exists = false; + return; + } + + this.server_already_exists = true; + + // check whether client has joined the stream + if (!this.started) { return; } + + if (!stream['playing'] && this.playing) { + // tell client to pause and set the timestamp to sync + this.togglePlayback.emit(false); + this.setPlaybackTimestamp.emit(stream['playback_timestamp']); + } else if (stream['playing']) { + // sync unpause state + if (!this.playing) { this.togglePlayback.emit(true); } + + // sync time + const zeroed_local_unix_timestamp = current_unix_timestamp - current_playback_timestamp; + const zeroed_server_unix_timestamp = stream['unix_timestamp'] - stream['playback_timestamp']; + + const seconds_behind_locally = zeroed_local_unix_timestamp - zeroed_server_unix_timestamp; + + if (Math.abs(seconds_behind_locally) > this.PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_SKIP) { + // skip to playback timestamp because the difference is too high + this.setPlaybackTimestamp.emit(this.playback_timestamp + seconds_behind_locally + 0.3); + this.playback_rate_modified = false; + } else if (!this.playback_rate_modified && Math.abs(seconds_behind_locally) > this.PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_PLAYBACK_MODIFICATION) { + // increase playback speed to avoid skipping + let seconds_to_wait = (Math.abs(seconds_behind_locally)/this.PLAYBACK_MODIFIER); + seconds_to_wait += 0.3/this.PLAYBACK_MODIFIER; + + this.playback_rate_modified = true; + + if (seconds_behind_locally > 0) { + // increase speed + this.setPlaybackRate.emit(1 + this.PLAYBACK_MODIFIER); + setTimeout(() => { + this.setPlaybackRate.emit(1); + this.playback_rate_modified = false; + }, seconds_to_wait * 1000); + } else { + // decrease speed + this.setPlaybackRate.emit(1 - this.PLAYBACK_MODIFIER); + setTimeout(() => { + this.setPlaybackRate.emit(1); + this.playback_rate_modified = false; + }, seconds_to_wait * 1000); + } + } + } + }); + } + + startWatching() { + this.watch_together_clicked = true; + this.check_timeout = setInterval(() => { + this.checkStream(); + }, 1000); + } + + stop() { + if (this.check_timeout) { clearInterval(this.check_timeout); } + if (this.update_timeout) { clearInterval(this.update_timeout); } + this.started = false; + this.server_started = false; + this.watch_together_clicked = false; + } + + +} From 46f85794391d981123b30ce7efd885af344f55f6 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 12 May 2021 22:56:16 -0600 Subject: [PATCH 056/680] Refactored player component to utilize uids instead of fileNames to improve maintainability, consistency, and reliability Playlists now use uids instead of fileNames Added generic getPlaylist and updatePlaylist functions --- backend/app.js | 164 +++++++------ backend/db.js | 86 ++++--- .../custom-playlists.component.ts | 16 +- .../recent-videos/recent-videos.component.ts | 5 +- .../create-playlist.component.html | 2 +- .../share-media-dialog.component.ts | 2 +- src/app/main/main.component.ts | 34 +-- src/app/player/player.component.html | 8 +- src/app/player/player.component.ts | 225 ++++++------------ src/app/posts.services.ts | 45 ++-- .../subscription-file-card.component.ts | 2 +- .../subscription/subscription.component.ts | 14 +- 12 files changed, 288 insertions(+), 315 deletions(-) diff --git a/backend/app.js b/backend/app.js index c5e2d11..24b5664 100644 --- a/backend/app.js +++ b/backend/app.js @@ -139,6 +139,8 @@ var updaterStatus = null; var timestamp_server_start = Date.now(); +const concurrentStreams = {}; + if (debugMode) logger.info('YTDL-Material in debug mode!'); // check if just updated @@ -1849,14 +1851,14 @@ const optionalJwt = function (req, res, next) { const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode'); if (multiUserMode && ((req.body && req.body.uuid) || (req.query && req.query.uuid)) && (req.path.includes('/api/getFile') || req.path.includes('/api/stream') || + req.path.includes('/api/getPlaylist') || req.path.includes('/api/downloadFile'))) { // check if shared video const using_body = req.body && req.body.uuid; const uuid = using_body ? req.body.uuid : req.query.uuid; const uid = using_body ? req.body.uid : req.query.uid; - const type = using_body ? req.body.type : req.query.type; - const playlist_id = using_body ? req.body.id : req.query.id; - const file = !playlist_id ? auth_api.getUserVideo(uuid, uid, type, true, req.body) : auth_api.getUserPlaylist(uuid, playlist_id, null, false); + const playlist_id = using_body ? req.body.playlist_id : req.query.playlist_id; + const file = !playlist_id ? auth_api.getUserVideo(uuid, uid, true) : db_api.getPlaylist(playlist_id, uuid, true); if (file) { req.can_watch = true; return next(); @@ -2118,6 +2120,34 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) { }); }); +app.post('/api/checkConcurrentStream', async (req, res) => { + const uid = req.body.uid; + + const DEAD_SERVER_THRESHOLD = 10; + + if (concurrentStreams[uid] && Date.now()/1000 - concurrentStreams[uid]['unix_timestamp'] > DEAD_SERVER_THRESHOLD) { + logger.verbose( `Killing dead stream on ${uid}`); + delete concurrentStreams[uid]; + } + + res.send({stream: concurrentStreams[uid]}) +}); + +app.post('/api/updateConcurrentStream', optionalJwt, async (req, res) => { + const uid = req.body.uid; + const playback_timestamp = req.body.playback_timestamp; + const unix_timestamp = req.body.unix_timestamp; + const playing = req.body.playing; + + concurrentStreams[uid] = { + playback_timestamp: playback_timestamp, + unix_timestamp: unix_timestamp, + playing: playing + } + + res.send({stream: concurrentStreams[uid]}) +}); + app.post('/api/getFullTwitchChat', optionalJwt, async (req, res) => { var id = req.body.id; var type = req.body.type; @@ -2174,7 +2204,7 @@ app.post('/api/enableSharing', optionalJwt, function(req, res) { // single-user mode try { success = true; - if (!is_playlist && type !== 'subscription') { + if (!is_playlist) { db.get(`files`) .find({uid: uid}) .assign({sharingEnabled: true}) @@ -2184,7 +2214,7 @@ app.post('/api/enableSharing', optionalJwt, function(req, res) { .find({id: uid}) .assign({sharingEnabled: true}) .write(); - } else if (type === 'subscription') { + } else if (false) { // TODO: Implement. Main blocker right now is subscription videos are not stored in the DB, they are searched for every // time they are requested from the subscription directory. } else { @@ -2193,6 +2223,7 @@ app.post('/api/enableSharing', optionalJwt, function(req, res) { } } catch(err) { + logger.error(err); success = false; } @@ -2525,14 +2556,14 @@ app.post('/api/getSubscriptions', optionalJwt, async (req, res) => { app.post('/api/createPlaylist', optionalJwt, async (req, res) => { let playlistName = req.body.playlistName; - let fileNames = req.body.fileNames; + let uids = req.body.uids; let type = req.body.type; let thumbnailURL = req.body.thumbnailURL; let duration = req.body.duration; let new_playlist = { name: playlistName, - fileNames: fileNames, + uids: uids, id: shortid.generate(), thumbnailURL: thumbnailURL, type: type, @@ -2556,15 +2587,19 @@ app.post('/api/createPlaylist', optionalJwt, async (req, res) => { }); app.post('/api/getPlaylist', optionalJwt, async (req, res) => { - let playlistID = req.body.playlistID; - let uuid = req.body.uuid; + let playlist_id = req.body.playlist_id; + let uuid = req.body.uuid ? req.body.uuid : (req.user && req.user.uid ? req.user.uid : null); + let include_file_metadata = req.body.include_file_metadata; - let playlist = null; + const playlist = await db_api.getPlaylist(playlist_id, uuid); - if (req.isAuthenticated()) { - playlist = auth_api.getUserPlaylist(uuid ? uuid : req.user.uid, playlistID); - } else { - playlist = db.get(`playlists`).find({id: playlistID}).value(); + if (playlist && include_file_metadata) { + playlist['file_objs'] = []; + for (let i = 0; i < playlist['uids'].length; i++) { + const uid = playlist['uids'][i]; + const file_obj = await db_api.getVideo(uid, uuid); + playlist['file_objs'].push(file_obj); + } } res.send({ @@ -2576,16 +2611,16 @@ app.post('/api/getPlaylist', optionalJwt, async (req, res) => { app.post('/api/updatePlaylistFiles', optionalJwt, async (req, res) => { let playlistID = req.body.playlistID; - let fileNames = req.body.fileNames; + let uids = req.body.uids; let success = false; try { if (req.isAuthenticated()) { - auth_api.updatePlaylistFiles(req.user.uid, playlistID, fileNames); + auth_api.updatePlaylistFiles(req.user.uid, playlistID, uids); } else { db.get(`playlists`) .find({id: playlistID}) - .assign({fileNames: fileNames}) + .assign({uids: uids}) .write(); } @@ -2664,51 +2699,36 @@ app.post('/api/deleteFile', optionalJwt, async (req, res) => { }); app.post('/api/downloadFile', optionalJwt, async (req, res) => { - let fileNames = req.body.fileNames; - let zip_mode = req.body.zip_mode; - let type = req.body.type; - let outputName = req.body.outputName; - let fullPathProvided = req.body.fullPathProvided; - let subscriptionName = req.body.subscriptionName; - let subscriptionPlaylist = req.body.subPlaylist; - let file = null; - if (!zip_mode) { - fileNames = decodeURIComponent(fileNames); - const is_audio = type === 'audio'; - const fileFolderPath = is_audio ? audioFolderPath : videoFolderPath; - const ext = is_audio ? '.mp3' : '.mp4'; + let uid = req.body.uid; + let is_playlist = req.body.is_playlist; + let uuid = req.body.uuid; - let base_path = fileFolderPath; - let usersFileFolder = null; - const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode'); - if (multiUserMode && (req.body.uuid || req.user.uid)) { - usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); - base_path = path.join(usersFileFolder, req.body.uuid ? req.body.uuid : req.user.uid, type); - } - if (!subscriptionName) { - file = path.join(__dirname, base_path, fileNames + ext); - } else { - let basePath = null; - if (usersFileFolder) - basePath = path.join(usersFileFolder, req.user.uid, 'subscriptions'); - else - basePath = config_api.getConfigItem('ytdl_subscriptions_base_path'); + let file_path_to_download = null; - file = path.join(__dirname, basePath, (subscriptionPlaylist === true || subscriptionPlaylist === 'true' ? 'playlists' : 'channels'), subscriptionName, fileNames + ext); + if (!uuid && req.user) uuid = req.user.uid; + if (is_playlist) { + const playlist_files_to_download = []; + const playlist = db_api.getPlaylist(uid, uuid); + for (let i = 0; i < playlist['uids'].length; i++) { + const uid = playlist['uids'][i]; + const file_obj = await db_api.getVideo(uid, uuid); + playlist_files_to_download.push(file_obj.path); } + + // generate zip + file_path_to_download = await createPlaylistZipFile(playlist_files_to_download, playlist.type, playlist.name); } else { - for (let i = 0; i < fileNames.length; i++) { - fileNames[i] = decodeURIComponent(fileNames[i]); - } - file = await createPlaylistZipFile(fileNames, type, outputName, fullPathProvided, req.body.uuid || req.user.uid); - if (!path.isAbsolute(file)) file = path.join(__dirname, file); + const file_obj = await db_api.getVideo(uid, uuid) + file_path_to_download = file_obj.path; } - res.sendFile(file, function (err) { + if (!path.isAbsolute(file_path_to_download)) file_path_to_download = path.join(__dirname, file_path_to_download); + res.sendFile(file_path_to_download, function (err) { if (err) { logger.error(err); - } else if (fullPathProvided) { + } else if (is_playlist) { try { - fs.unlinkSync(file); + // delete generated zip file + fs.unlinkSync(file_path_to_download); } catch(e) { logger.error("Failed to remove file", file); } @@ -2783,31 +2803,21 @@ app.post('/api/generateNewAPIKey', function (req, res) { // Streaming API calls -app.get('/api/stream/:id', optionalJwt, (req, res) => { +app.get('/api/stream', optionalJwt, async (req, res) => { const type = req.query.type; const ext = type === 'audio' ? '.mp3' : '.mp4'; const mimetype = type === 'audio' ? 'audio/mp3' : 'video/mp4'; var head; let optionalParams = url_api.parse(req.url,true).query; - let id = decodeURIComponent(req.params.id); - let file_path = req.query.file_path ? decodeURIComponent(req.query.file_path.split('?')[0]) : null; - if (!file_path && (req.isAuthenticated() || req.can_watch)) { - let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); - if (optionalParams['subName']) { - const isPlaylist = optionalParams['subPlaylist']; - file_path = path.join(usersFileFolder, req.user.uid, 'subscriptions', (isPlaylist === 'true' ? 'playlists/' : 'channels/'),optionalParams['subName'], id + ext) - } else { - file_path = path.join(usersFileFolder, req.query.uuid ? req.query.uuid : req.user.uid, type, id + ext); - } - } else if (!file_path && optionalParams['subName']) { - let basePath = config_api.getConfigItem('ytdl_subscriptions_base_path'); - const isPlaylist = optionalParams['subPlaylist']; - basePath += (isPlaylist === 'true' ? 'playlists/' : 'channels/'); - file_path = basePath + optionalParams['subName'] + '/' + id + ext; - } + let uid = decodeURIComponent(req.query.uid); - if (!file_path) { - file_path = path.join(type === 'audio' ? audioFolderPath : videoFolderPath, id + ext); + let file_path = null; + + const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode'); + if (!multiUserMode || req.isAuthenticated() || req.can_watch) { + const file_obj = await db_api.getVideo(uid, req.query.uuid ? req.query.uuid : (req.user ? req.user.uid : null), req.query.sub_id); + if (file_obj) file_path = file_obj['path']; + else file_path = null; } const stat = fs.statSync(file_path) @@ -2821,11 +2831,11 @@ app.get('/api/stream/:id', optionalJwt, (req, res) => { : fileSize-1 const chunksize = (end-start)+1 const file = fs.createReadStream(file_path, {start, end}) - if (config_api.descriptors[id]) config_api.descriptors[id].push(file); - else config_api.descriptors[id] = [file]; + if (config_api.descriptors[uid]) config_api.descriptors[uid].push(file); + else config_api.descriptors[uid] = [file]; file.on('close', function() { - let index = config_api.descriptors[id].indexOf(file); - config_api.descriptors[id].splice(index, 1); + let index = config_api.descriptors[uid].indexOf(file); + config_api.descriptors[uid].splice(index, 1); logger.debug('Successfully closed stream and removed file reference.'); }); head = { diff --git a/backend/db.js b/backend/db.js index c8ef8fb..061280b 100644 --- a/backend/db.js +++ b/backend/db.js @@ -10,12 +10,12 @@ var users_db = null; function setDB(input_db, input_users_db) { db = input_db; users_db = input_users_db } function setLogger(input_logger) { logger = input_logger; } -function initialize(input_db, input_users_db, input_logger) { +exports.initialize = (input_db, input_users_db, input_logger) => { setDB(input_db, input_users_db); setLogger(input_logger); } -function registerFileDB(file_path, type, multiUserMode = null, sub = null, customPath = null, category = null, cropFileSettings = null) { +exports.registerFileDB = (file_path, type, multiUserMode = null, sub = null, customPath = null, category = null, cropFileSettings = null) => { let db_path = null; const file_id = utils.removeFileExtension(file_path); const file_object = generateFileObject(file_id, type, customPath || multiUserMode && multiUserMode.file_path, sub); @@ -107,23 +107,11 @@ function generateFileObject(id, type, customPath = null, sub = null) { return file_obj; } -function updatePlaylist(playlist, user_uid) { - let playlistID = playlist.id; - let db_loc = null; - if (user_uid) { - db_loc = users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlistID}); - } else { - db_loc = db.get(`playlists`).find({id: playlistID}); - } - db_loc.assign(playlist).write(); - return true; -} - function getAppendedBasePathSub(sub, base_path) { return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name); } -function getFileDirectoriesAndDBs() { +exports.getFileDirectoriesAndDBs = () => { let dirs_to_check = []; let subscriptions_to_check = []; const subscriptions_base_path = config_api.getConfigItem('ytdl_subscriptions_base_path'); // only for single-user mode @@ -192,8 +180,8 @@ function getFileDirectoriesAndDBs() { return dirs_to_check; } -async function importUnregisteredFiles() { - const dirs_to_check = getFileDirectoriesAndDBs(); +exports.importUnregisteredFiles = async () => { + const dirs_to_check = exports.getFileDirectoriesAndDBs(); // run through check list and check each file to see if it's missing from the db for (const dir_to_check of dirs_to_check) { @@ -213,7 +201,7 @@ async function importUnregisteredFiles() { } -async function preimportUnregisteredSubscriptionFile(sub, appendedBasePath) { +exports.preimportUnregisteredSubscriptionFile = async (sub, appendedBasePath) => { const preimported_file_paths = []; let dbPath = null; @@ -236,13 +224,60 @@ async function preimportUnregisteredSubscriptionFile(sub, appendedBasePath) { return preimported_file_paths; } -async function getVideo(file_uid, uuid, sub_id) { +exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = false) => { + let playlist = null + if (user_uid) { + playlist = users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlist_id}).value(); + + // prevent unauthorized users from accessing the file info + if (require_sharing && !playlist['sharingEnabled']) return null; + } else { + playlist = db.get(`playlists`).find({id: playlist_id}).value(); + } + + // converts playlists to new UID-based schema + if (playlist && playlist['fileNames'] && !playlist['uids']) { + playlist['uids'] = []; + logger.verbose(`Converting playlist ${playlist['name']} to new UID-based schema.`); + for (let i = 0; i < playlist['fileNames'].length; i++) { + const fileName = playlist['fileNames'][i]; + const uid = exports.getVideoUIDByID(fileName, user_uid); + if (uid) playlist['uids'].push(uid); + else logger.warn(`Failed to convert file with name ${fileName} to its UID while converting playlist ${playlist['name']} to the new UID-based schema. The original file is likely missing/deleted and it will be skipped.`); + } + delete playlist['fileNames']; + exports.updatePlaylist(playlist, user_uid); + } + + return playlist; +} + +exports.updatePlaylist = (playlist, user_uid = null) => { + let playlistID = playlist.id; + let db_loc = null; + if (user_uid) { + db_loc = users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlistID}); + } else { + db_loc = db.get(`playlists`).find({id: playlistID}); + } + db_loc.assign(playlist).write(); + return true; +} + +// Video ID is basically just the file name without the base path and file extension - this method helps us get away from that +exports.getVideoUIDByID = (file_id, uuid = null) => { + const base_db_path = uuid ? users_db.get('users').find({uid: uuid}) : db; + const file_obj = base_db_path.get('files').find({id: file_id}).value(); + return file_obj ? file_obj['uid'] : null; +} + +exports.getVideo = async (file_uid, uuid, sub_id) => { const base_db_path = uuid ? users_db.get('users').find({uid: uuid}) : db; const sub_db_path = sub_id ? base_db_path.get('subscriptions').find({id: sub_id}).get('videos') : base_db_path.get('files'); return sub_db_path.find({uid: file_uid}).value(); } -async function setVideoProperty(file_uid, assignment_obj, uuid, sub_id) { +exports.setVideoProperty = async (file_uid, assignment_obj, uuid, sub_id) => { const base_db_path = uuid ? users_db.get('users').find({uid: uuid}) : db; const sub_db_path = sub_id ? base_db_path.get('subscriptions').find({id: sub_id}).get('videos') : base_db_path.get('files'); const file_db_path = sub_db_path.find({uid: file_uid}); @@ -251,14 +286,3 @@ async function setVideoProperty(file_uid, assignment_obj, uuid, sub_id) { } sub_db_path.find({uid: file_uid}).assign(assignment_obj).write(); } - -module.exports = { - initialize: initialize, - registerFileDB: registerFileDB, - updatePlaylist: updatePlaylist, - getFileDirectoriesAndDBs: getFileDirectoriesAndDBs, - importUnregisteredFiles: importUnregisteredFiles, - preimportUnregisteredSubscriptionFile: preimportUnregisteredSubscriptionFile, - getVideo: getVideo, - setVideoProperty: setVideoProperty -} diff --git a/src/app/components/custom-playlists/custom-playlists.component.ts b/src/app/components/custom-playlists/custom-playlists.component.ts index 73e3036..8870a8e 100644 --- a/src/app/components/custom-playlists/custom-playlists.component.ts +++ b/src/app/components/custom-playlists/custom-playlists.component.ts @@ -57,12 +57,11 @@ export class CustomPlaylistsComponent implements OnInit { if (playlist) { if (this.postsService.config['Extra']['download_only_mode']) { - this.downloading_content[type][playlistID] = true; - this.downloadPlaylist(playlist.fileNames, type, playlist.name, playlistID); + this.downloadPlaylist(playlist.id, playlist.name); } else { localStorage.setItem('player_navigator', this.router.url); const fileNames = playlist.fileNames; - this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID, uid: playlistID, auto: playlist.auto}]); + this.router.navigate(['/player', {playlist_id: playlistID, auto: playlist.auto}]); } } else { // playlist not found @@ -70,11 +69,12 @@ export class CustomPlaylistsComponent implements OnInit { } } - downloadPlaylist(fileNames, type, zipName = null, playlistID = null) { - this.postsService.downloadFileFromServer(fileNames, type, zipName).subscribe(res => { - if (playlistID) { this.downloading_content[type][playlistID] = false }; - const blob: Blob = res; - saveAs(blob, zipName + '.zip'); + downloadPlaylist(playlist_id, playlist_name) { + this.downloading_content[playlist_id] = true; + this.postsService.downloadPlaylistFromServer(playlist_id).subscribe(res => { + this.downloading_content[playlist_id] = false; + const blob: any = res; + saveAs(blob, playlist_name + '.zip'); }); } diff --git a/src/app/components/recent-videos/recent-videos.component.ts b/src/app/components/recent-videos/recent-videos.component.ts index 6aec7f2..2b8fe94 100644 --- a/src/app/components/recent-videos/recent-videos.component.ts +++ b/src/app/components/recent-videos/recent-videos.component.ts @@ -201,8 +201,7 @@ export class RecentVideosComponent implements OnInit { const type = file.isAudio ? 'audio' : 'video'; const ext = type === 'audio' ? '.mp3' : '.mp4' const sub = this.postsService.getSubscriptionByID(file.sub_id); - this.postsService.downloadFileFromServer(file.id, type, null, null, sub.name, sub.isPlaylist, - this.postsService.user ? this.postsService.user.uid : null, null).subscribe(res => { + this.postsService.downloadFileFromServer(file.uid).subscribe(res => { const blob: Blob = res; saveAs(blob, file.id + ext); }, err => { @@ -215,7 +214,7 @@ export class RecentVideosComponent implements OnInit { const ext = type === 'audio' ? '.mp3' : '.mp4' const name = file.id; this.downloading_content[type][name] = true; - this.postsService.downloadFileFromServer(name, type).subscribe(res => { + this.postsService.downloadFileFromServer(file.uid).subscribe(res => { this.downloading_content[type][name] = false; const blob: Blob = res; saveAs(blob, decodeURIComponent(name) + ext); diff --git a/src/app/create-playlist/create-playlist.component.html b/src/app/create-playlist/create-playlist.component.html index 8027983..d9f108a 100644 --- a/src/app/create-playlist/create-playlist.component.html +++ b/src/app/create-playlist/create-playlist.component.html @@ -19,7 +19,7 @@ Audio files Videos - {{file.id}} + {{file.id}} {{file.id}} {{file.id}} diff --git a/src/app/dialogs/share-media-dialog/share-media-dialog.component.ts b/src/app/dialogs/share-media-dialog/share-media-dialog.component.ts index 137e8fc..9b687ff 100644 --- a/src/app/dialogs/share-media-dialog/share-media-dialog.component.ts +++ b/src/app/dialogs/share-media-dialog/share-media-dialog.component.ts @@ -33,7 +33,7 @@ export class ShareMediaDialogComponent implements OnInit { this.is_playlist = this.data.is_playlist; this.current_timestamp = (this.data.current_timestamp / 1000).toFixed(2); - const arg = (this.is_playlist ? ';id=' : ';uid='); + const arg = (this.is_playlist ? ';playlist_id=' : ';uid='); this.default_share_url = window.location.href.split(';')[0] + arg + this.uid; if (this.uuid) { this.default_share_url += ';uuid=' + this.uuid; diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts index 51a90ce..e37d041 100644 --- a/src/app/main/main.component.ts +++ b/src/app/main/main.component.ts @@ -355,7 +355,7 @@ export class MainComponent implements OnInit { if (playlist) { if (this.downloadOnlyMode) { this.downloading_content[type][playlistID] = true; - this.downloadPlaylist(playlist.fileNames, type, playlist.name, playlistID); + this.downloadPlaylist(playlist); } else { localStorage.setItem('player_navigator', this.router.url); const fileNames = playlist.fileNames; @@ -626,41 +626,41 @@ export class MainComponent implements OnInit { } } - downloadAudioFile(name) { - this.downloading_content['audio'][name] = true; - this.postsService.downloadFileFromServer(name, 'audio').subscribe(res => { - this.downloading_content['audio'][name] = false; + downloadAudioFile(file) { + this.downloading_content['audio'][file.id] = true; + this.postsService.downloadFileFromServer(file.uid).subscribe(res => { + this.downloading_content['audio'][file.id] = false; const blob: Blob = res; - saveAs(blob, decodeURIComponent(name) + '.mp3'); + saveAs(blob, decodeURIComponent(file.id) + '.mp3'); if (!this.fileManagerEnabled) { // tell server to delete the file once downloaded - this.postsService.deleteFile(name, 'video').subscribe(delRes => { + this.postsService.deleteFile(file.uid).subscribe(delRes => { }); } }); } - downloadVideoFile(name) { - this.downloading_content['video'][name] = true; - this.postsService.downloadFileFromServer(name, 'video').subscribe(res => { - this.downloading_content['video'][name] = false; + downloadVideoFile(file) { + this.downloading_content['video'][file.id] = true; + this.postsService.downloadFileFromServer(file.uid).subscribe(res => { + this.downloading_content['video'][file.id] = false; const blob: Blob = res; - saveAs(blob, decodeURIComponent(name) + '.mp4'); + saveAs(blob, decodeURIComponent(file.id) + '.mp4'); if (!this.fileManagerEnabled) { // tell server to delete the file once downloaded - this.postsService.deleteFile(name, 'audio').subscribe(delRes => { + this.postsService.deleteFile(file.uid).subscribe(delRes => { }); } }); } - downloadPlaylist(fileNames, type, zipName = null, playlistID = null) { - this.postsService.downloadFileFromServer(fileNames, type, zipName).subscribe(res => { - if (playlistID) { this.downloading_content[type][playlistID] = false }; + downloadPlaylist(playlist) { + this.postsService.downloadFileFromServer(playlist.id, null, true).subscribe(res => { + if (playlist.id) { this.downloading_content[playlist.type][playlist.id] = false }; const blob: Blob = res; - saveAs(blob, zipName + '.zip'); + saveAs(blob, playlist.name + '.zip'); }); } diff --git a/src/app/player/player.component.html b/src/app/player/player.component.html index 7f8d928..9de791a 100644 --- a/src/app/player/player.component.html +++ b/src/app/player/player.component.html @@ -29,12 +29,11 @@
- - + - + @@ -47,6 +46,9 @@ {{playlist_item.label}}
+ + + diff --git a/src/app/player/player.component.ts b/src/app/player/player.component.ts index b3660a5..601271d 100644 --- a/src/app/player/player.component.ts +++ b/src/app/player/player.component.ts @@ -36,18 +36,16 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { api_ready = false; // params - fileNames: string[]; + uids: string[]; type: string; - id = null; // used for playlists (not subscription) + playlist_id = null; // used for playlists (not subscription) uid = null; // used for non-subscription files (audio, video, playlist) subscription = null; - subscriptionName = null; + sub_id = null; subPlaylist = null; uuid = null; // used for sharing in multi-user mode, uuid is the user that downloaded the video timestamp = null; - is_shared = false; - db_playlist = null; db_file = null; @@ -56,8 +54,6 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { videoFolderPath = null; subscriptionFolderPath = null; - sharingEnabled = null; - // url-mode params url = null; name = null; @@ -79,11 +75,9 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { ngOnInit(): void { this.innerWidth = window.innerWidth; - this.type = this.route.snapshot.paramMap.get('type'); - this.id = this.route.snapshot.paramMap.get('id'); + this.playlist_id = this.route.snapshot.paramMap.get('playlist_id'); this.uid = this.route.snapshot.paramMap.get('uid'); - this.subscriptionName = this.route.snapshot.paramMap.get('subscriptionName'); - this.subPlaylist = this.route.snapshot.paramMap.get('subPlaylist'); + this.sub_id = this.route.snapshot.paramMap.get('sub_id'); this.url = this.route.snapshot.paramMap.get('url'); this.name = this.route.snapshot.paramMap.get('name'); this.uuid = this.route.snapshot.paramMap.get('uuid'); @@ -120,19 +114,14 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { this.audioFolderPath = this.postsService.config['Downloader']['path-audio']; this.videoFolderPath = this.postsService.config['Downloader']['path-video']; this.subscriptionFolderPath = this.postsService.config['Subscriptions']['subscriptions_base_path']; - this.fileNames = this.route.snapshot.paramMap.get('fileNames') ? this.route.snapshot.paramMap.get('fileNames').split('|nvr|') : null; - if (!this.fileNames && !this.type) { - this.is_shared = true; - } - - if (this.uid && !this.id) { - this.getFile(); - } else if (this.id) { - this.getPlaylistFiles(); - } else if (this.subscriptionName) { + if (this.sub_id) { this.getSubscription(); - } + } else if (this.playlist_id) { + this.getPlaylistFiles(); + } else if (this.uid) { + this.getFile(); + } if (this.url) { // if a url is given, just stream the URL @@ -147,14 +136,10 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { this.currentItem = this.playlist[0]; this.currentIndex = 0; this.show_player = true; - } else if (this.fileNames && !this.subscriptionName) { - this.show_player = true; - this.parseFileNames(); } } getFile() { - const already_has_filenames = !!this.fileNames; this.postsService.getFile(this.uid, null, this.uuid).subscribe(res => { this.db_file = res['file']; if (!this.db_file) { @@ -165,45 +150,32 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { console.error('Failed to increment view count'); console.error(err); }); - this.sharingEnabled = this.db_file.sharingEnabled; - if (!this.fileNames) { - // means it's a shared video - if (!this.id) { - // regular video/audio file (not playlist) - this.fileNames = [this.db_file['id']]; - this.type = this.db_file['isAudio'] ? 'audio' : 'video'; - if (!already_has_filenames) { this.parseFileNames(); } - } - } - if (this.db_file['sharingEnabled'] || !this.uuid) { - this.show_player = true; - } else if (!already_has_filenames) { - this.openSnackBar('Error: Sharing has been disabled for this video!', 'Dismiss'); - } + // regular video/audio file (not playlist) + this.uids = [this.db_file['uid']]; + this.type = this.db_file['isAudio'] ? 'audio' : 'video'; + this.parseFileNames(); }); } getSubscription() { - this.postsService.getSubscription(null, this.subscriptionName).subscribe(res => { + this.postsService.getSubscription(this.sub_id).subscribe(res => { const subscription = res['subscription']; this.subscription = subscription; - if (this.fileNames) { - subscription.videos.forEach(video => { - if (video['id'] === this.fileNames[0]) { - this.db_file = video; - this.postsService.incrementViewCount(this.db_file['uid'], this.subscription['id'], this.uuid).subscribe(res => {}, err => { - console.error('Failed to increment view count'); - console.error(err); - }); - this.show_player = true; - this.parseFileNames(); - } - }); - } else { - console.log('no file name specified'); - } + this.type === this.subscription.type; + subscription.videos.forEach(video => { + if (video['uid'] === this.uid) { + this.db_file = video; + this.postsService.incrementViewCount(this.db_file['uid'], this.sub_id, this.uuid).subscribe(res => {}, err => { + console.error('Failed to increment view count'); + console.error(err); + }); + this.uids = this.db_file['uid']; + this.show_player = true; + this.parseFileNames(); + } + }); }, err => { - this.openSnackBar(`Failed to find subscription ${this.subscriptionName}`, 'Dismiss'); + this.openSnackBar(`Failed to find subscription ${this.sub_id}`, 'Dismiss'); }); } @@ -212,10 +184,10 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { this.show_player = true; return; } - this.postsService.getPlaylist(this.id, null, this.uuid).subscribe(res => { + this.postsService.getPlaylist(this.playlist_id, this.uuid, true).subscribe(res => { if (res['playlist']) { this.db_playlist = res['playlist']; - this.fileNames = this.db_playlist['fileNames']; + this.uids = this.db_playlist.uids; this.type = res['type']; this.show_player = true; this.parseFileNames(); @@ -231,60 +203,43 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { let fileType = null; if (this.type === 'audio') { fileType = 'audio/mp3'; - } else if (this.type === 'video') { - fileType = 'video/mp4'; } else { - // error - console.error('Must have valid file type! Use \'audio\', \'video\', or \'subscription\'.'); + fileType = 'video/mp4'; } this.playlist = []; - for (let i = 0; i < this.fileNames.length; i++) { - const fileName = this.fileNames[i]; - let baseLocation = null; - let fullLocation = null; + for (let i = 0; i < this.uids.length; i++) { + const uid = this.uids[i]; - // adds user token if in multi-user-mode - const uuid_str = this.uuid ? `&uuid=${this.uuid}` : ''; - const uid_str = (this.id || !this.db_file) ? '' : `&uid=${this.db_file.uid}`; - const type_str = (this.type || !this.db_file) ? `&type=${this.type}` : `&type=${this.db_file.type}` - const id_str = this.id ? `&id=${this.id}` : ''; - const file_path_str = (!this.db_file) ? '' : `&file_path=${encodeURIComponent(this.db_file.path)}`; + const file_obj = this.playlist_id ? this.db_playlist['file_objs'][i] : this.db_file; - if (!this.subscriptionName) { - baseLocation = 'stream/'; - fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + `?test=test${type_str}${file_path_str}`; - } else { - // default to video but include subscription name param - baseLocation = 'stream/'; - fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + '?subName=' + this.subscriptionName + - '&subPlaylist=' + this.subPlaylist + `${file_path_str}${type_str}`; - } + let baseLocation = 'stream/'; + let fullLocation = this.baseStreamPath + baseLocation + `?test=test&uid=${file_obj['uid']}`; if (this.postsService.isLoggedIn) { - fullLocation += (this.subscriptionName ? '&' : '&') + `jwt=${this.postsService.token}`; - if (this.is_shared) { fullLocation += `${uuid_str}${uid_str}${type_str}${id_str}`; } - } else if (this.is_shared) { - fullLocation += (this.subscriptionName ? '&' : '?') + `test=test${uuid_str}${uid_str}${type_str}${id_str}`; + fullLocation += `&jwt=${this.postsService.token}`; } - // if it has a slash (meaning it's in a directory), only get the file name for the label - let label = null; - const decodedName = decodeURIComponent(fileName); - const hasSlash = decodedName.includes('/') || decodedName.includes('\\'); - if (hasSlash) { - label = decodedName.replace(/^.*[\\\/]/, ''); - } else { - label = decodedName; + + if (this.uuid) { + fullLocation += `&uuid=${this.uuid}`; } + + if (this.sub_id) { + fullLocation += `&sub_id=${this.sub_id}`; + } else if (this.playlist_id) { + fullLocation += `&playlist_id=${this.playlist_id}`; + } + const mediaObject: IMedia = { - title: fileName, + title: file_obj['title'], src: fullLocation, type: fileType, - label: label + label: file_obj['title'] } this.playlist.push(mediaObject); } this.currentItem = this.playlist[this.currentIndex]; this.original_playlist = JSON.stringify(this.playlist); + this.show_player = true; } onPlayerReady(api: VgApiService) { @@ -361,8 +316,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { const zipName = fileNames[0].split(' ')[0] + fileNames[1].split(' ')[0]; this.downloading = true; - this.postsService.downloadFileFromServer(fileNames, this.type, zipName, null, null, null, null, - !this.uuid ? this.postsService.user.uid : this.uuid, this.id).subscribe(res => { + this.postsService.downloadFileFromServer(this.playlist_id, this.uuid, true).subscribe(res => { this.downloading = false; const blob: Blob = res; saveAs(blob, zipName + '.zip'); @@ -376,8 +330,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { const ext = (this.type === 'audio') ? '.mp3' : '.mp4'; const filename = this.playlist[0].title; this.downloading = true; - this.postsService.downloadFileFromServer(filename, this.type, null, null, this.subscriptionName, this.subPlaylist, - this.is_shared ? this.db_file['uid'] : null, this.uuid).subscribe(res => { + this.postsService.downloadFileFromServer(this.uid, this.uuid, false).subscribe(res => { this.downloading = false; const blob: Blob = res; saveAs(blob, filename + ext); @@ -387,50 +340,10 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { }); } - namePlaylistDialog() { - const done = new EventEmitter(); - const dialogRef = this.dialog.open(InputDialogComponent, { - width: '300px', - data: { - inputTitle: 'Name the playlist', - inputPlaceholder: 'Name', - submitText: 'Favorite', - doneEmitter: done - } - }); - - done.subscribe(name => { - - // Eventually do additional checks on name - if (name) { - const fileNames = this.getFileNames(); - this.postsService.createPlaylist(name, fileNames, this.type, null).subscribe(res => { - if (res['success']) { - dialogRef.close(); - const new_playlist = res['new_playlist']; - this.db_playlist = new_playlist; - this.openSnackBar('Playlist \'' + name + '\' successfully created!', '') - this.playlistPostCreationHandler(new_playlist.id); - } - }); - } - }); - } - - /* - createPlaylist(name) { - this.postsService.createPlaylist(name, this.fileNames, this.type, null).subscribe(res => { - if (res['success']) { - console.log('Success!'); - } - }); - } - */ - playlistPostCreationHandler(playlistID) { // changes the route without moving from the current view or // triggering a navigation event - this.id = playlistID; + this.playlist_id = playlistID; this.router.navigateByUrl(this.router.url + ';id=' + playlistID); } @@ -445,11 +358,11 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { updatePlaylist() { const fileNames = this.getFileNames(); this.playlist_updating = true; - this.postsService.updatePlaylistFiles(this.id, fileNames, this.type).subscribe(res => { + this.postsService.updatePlaylistFiles(this.playlist_id, fileNames, this.type).subscribe(res => { this.playlist_updating = false; if (res['success']) { const fileNamesEncoded = fileNames.join('|nvr|'); - this.router.navigate(['/player', {fileNames: fileNamesEncoded, type: this.type, id: this.id}]); + this.router.navigate(['/player', {fileNames: fileNamesEncoded, type: this.type, id: this.playlist_id}]); this.openSnackBar('Successfully updated playlist.', ''); this.original_playlist = JSON.stringify(this.playlist); } else { @@ -461,10 +374,10 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { openShareDialog() { const dialogRef = this.dialog.open(ShareMediaDialogComponent, { data: { - uid: this.id ? this.id : this.uid, + uid: this.playlist_id ? this.playlist_id : this.uid, type: this.type, - sharing_enabled: this.id ? this.db_playlist.sharingEnabled : this.db_file.sharingEnabled, - is_playlist: !!this.id, + sharing_enabled: this.playlist_id ? this.db_playlist.sharingEnabled : this.db_file.sharingEnabled, + is_playlist: !!this.playlist_id, uuid: this.postsService.isLoggedIn ? this.postsService.user.uid : null, current_timestamp: this.api.time.current }, @@ -472,7 +385,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { }); dialogRef.afterClosed().subscribe(res => { - if (!this.id) { + if (!this.playlist_id) { this.getFile(); } else { this.getPlaylistFiles(); @@ -489,6 +402,22 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { }) } + setPlaybackTimestamp(time) { + this.api.seekTime(time); + } + + togglePlayback(to_play) { + if (to_play) { + this.api.play(); + } else { + this.api.pause(); + } + } + + setPlaybackRate(speed) { + this.api.playbackRate = speed; + } + // snackbar helper public openSnackBar(message: string, action: string) { this.snackBar.open(message, action, { diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 1fd7929..cf04447 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -219,8 +219,8 @@ export class PostsService implements CanActivate { return this.http.post(this.path + 'setConfig', {new_config_file: config}, this.httpOptions); } - deleteFile(uid: string, type: string, blacklistMode = false) { - return this.http.post(this.path + 'deleteFile', {uid: uid, type: type, blacklistMode: blacklistMode}, this.httpOptions); + deleteFile(uid: string, blacklistMode = false) { + return this.http.post(this.path + 'deleteFile', {uid: uid, blacklistMode: blacklistMode}, this.httpOptions); } getMp3s() { @@ -247,22 +247,30 @@ export class PostsService implements CanActivate { return this.http.post(this.path + 'downloadTwitchChatByVODID', {id: id, type: type, vodId: vodId, uuid: uuid, sub: sub}, this.httpOptions); } - downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null, subscriptionName = null, subPlaylist = null, - uid = null, uuid = null, id = null) { - return this.http.post(this.path + 'downloadFile', {fileNames: fileName, - type: type, - zip_mode: Array.isArray(fileName), - outputName: outputName, - fullPathProvided: fullPathProvided, - subscriptionName: subscriptionName, - subPlaylist: subPlaylist, - uuid: uuid, + downloadFileFromServer(uid, uuid = null, is_playlist = false) { + return this.http.post(this.path + 'downloadFile', { uid: uid, - id: id + uuid: uuid, + is_playlist: is_playlist }, {responseType: 'blob', params: this.httpOptions.params}); } + downloadPlaylistFromServer(playlist_id, uuid = null) { + return this.http.post(this.path + 'downloadPlaylist', {playlist_id: playlist_id, uuid: uuid}); + } + + checkConcurrentStream(uid) { + return this.http.post(this.path + 'checkConcurrentStream', {uid: uid}, this.httpOptions); + } + + updateConcurrentStream(uid, playback_timestamp, unix_timestamp, playing) { + return this.http.post(this.path + 'updateConcurrentStream', {uid: uid, + playback_timestamp: playback_timestamp, + unix_timestamp: unix_timestamp, + playing: playing}, this.httpOptions); + } + uploadCookiesFile(fileFormData) { return this.http.post(this.path + 'uploadCookies', fileFormData, this.httpOptions); } @@ -299,17 +307,18 @@ export class PostsService implements CanActivate { return this.http.post(this.path + 'disableSharing', {uid: uid, type: type, is_playlist: is_playlist}, this.httpOptions); } - createPlaylist(playlistName, fileNames, type, thumbnailURL, duration = null) { + createPlaylist(playlistName, uids, type, thumbnailURL, duration = null) { return this.http.post(this.path + 'createPlaylist', {playlistName: playlistName, - fileNames: fileNames, + uids: uids, type: type, thumbnailURL: thumbnailURL, duration: duration}, this.httpOptions); } - getPlaylist(playlistID, type, uuid = null) { - return this.http.post(this.path + 'getPlaylist', {playlistID: playlistID, - type: type, uuid: uuid}, this.httpOptions); + getPlaylist(playlist_id, uuid = null, include_file_metadata = false) { + return this.http.post(this.path + 'getPlaylist', {playlist_id: playlist_id, + uuid: uuid, + include_file_metadata: include_file_metadata}, this.httpOptions); } updatePlaylist(playlist) { diff --git a/src/app/subscription/subscription-file-card/subscription-file-card.component.ts b/src/app/subscription/subscription-file-card/subscription-file-card.component.ts index e500a7d..2257fd8 100644 --- a/src/app/subscription/subscription-file-card/subscription-file-card.component.ts +++ b/src/app/subscription/subscription-file-card/subscription-file-card.component.ts @@ -42,7 +42,7 @@ export class SubscriptionFileCardComponent implements OnInit { goToFile() { const emit_obj = { - name: this.file.id, + uid: this.file.uid, url: this.file.requested_formats ? this.file.requested_formats[0].url : this.file.url } this.goToFileEmit.emit(emit_obj); diff --git a/src/app/subscription/subscription/subscription.component.ts b/src/app/subscription/subscription/subscription.component.ts index 461dcfc..af08088 100644 --- a/src/app/subscription/subscription/subscription.component.ts +++ b/src/app/subscription/subscription/subscription.component.ts @@ -103,15 +103,14 @@ export class SubscriptionComponent implements OnInit, OnDestroy { } goToFile(emit_obj) { - const name = emit_obj['name']; + const uid = emit_obj['uid']; const url = emit_obj['url']; localStorage.setItem('player_navigator', this.router.url); if (this.subscription.streamingOnly) { - this.router.navigate(['/player', {name: name, url: url}]); + this.router.navigate(['/player', {uid: uid, url: url}]); } else { - this.router.navigate(['/player', {fileNames: name, - type: this.subscription.type ? this.subscription.type : 'video', subscriptionName: this.subscription.name, - subPlaylist: this.subscription.isPlaylist}]); + this.router.navigate(['/player', {uid: uid, + sub_id: this.subscription.id}]); } } @@ -154,14 +153,15 @@ export class SubscriptionComponent implements OnInit, OnDestroy { } this.downloading = true; - this.postsService.downloadFileFromServer(fileNames, 'video', this.subscription.name, true).subscribe(res => { + // TODO: add download subscription route + /*this.postsService.downloadFileFromServer(fileNames, 'video', this.subscription.name, true).subscribe(res => { this.downloading = false; const blob: Blob = res; saveAs(blob, this.subscription.name + '.zip'); }, err => { console.log(err); this.downloading = false; - }); + });*/ } editSubscription() { From 1d2ab0dc41e20215919ba31f8aae9367107c3389 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 12 May 2021 22:56:38 -0600 Subject: [PATCH 057/680] 401 errors will now not cause redirects in the /player route --- src/app/http.interceptor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/http.interceptor.ts b/src/app/http.interceptor.ts index edde22b..b941a34 100644 --- a/src/app/http.interceptor.ts +++ b/src/app/http.interceptor.ts @@ -14,7 +14,7 @@ export class H401Interceptor implements HttpInterceptor { return next.handle(request).pipe(catchError(err => { if (err.status === 401) { localStorage.setItem('jwt_token', null); - if (this.router.url !== '/login') { + if (this.router.url !== '/login' && !this.router.url.includes('player')) { this.router.navigate(['/login']).then(() => { this.openSnackBar('Login expired, please login again.'); }); From 297a4a3f34541c405d5d7f3c06e384e62476f256 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 16 May 2021 02:53:36 -0600 Subject: [PATCH 058/680] Simplified streaming and file deletion functions --- backend/app.js | 142 ++++-------------- .../recent-videos/recent-videos.component.ts | 2 +- src/app/file-card/file-card.component.ts | 2 +- 3 files changed, 32 insertions(+), 114 deletions(-) diff --git a/backend/app.js b/backend/app.js index 24b5664..8ae57ac 100644 --- a/backend/app.js +++ b/backend/app.js @@ -888,18 +888,22 @@ async function createPlaylistZipFile(fileNames, type, outputName, fullPathProvid return path.join(zipFolderPath,outputName + '.zip'); } -async function deleteAudioFile(name, customPath = null, blacklistMode = false) { - let filePath = customPath ? customPath : audioFolderPath; +// TODO: add to db_api and support multi-user mode +async function deleteFile(uid, uuid = null, blacklistMode = false) { + const file_obj = await db_api.getVideo(uid, uuid); + const type = file_obj.isAudio ? 'audio' : 'video'; + const folderPath = path.dirname(file_obj.path); + const ext = type === 'audio' ? 'mp3' : 'mp4'; + const name = file_obj.id; + const filePathNoExtension = utils.removeFileExtension(file_obj.path); - var jsonPath = path.join(filePath,name+'.mp3.info.json'); - var altJSONPath = path.join(filePath,name+'.info.json'); - var audioFilePath = path.join(filePath,name+'.mp3'); - var thumbnailPath = path.join(filePath,name+'.webp'); - var altThumbnailPath = path.join(filePath,name+'.jpg'); + var jsonPath = `${file_obj.path}.info.json`; + var altJSONPath = `${filePathNoExtension}.info.json`; + var thumbnailPath = `${filePathNoExtension}.webp`; + var altThumbnailPath = `${filePathNoExtension}.jpg`; jsonPath = path.join(__dirname, jsonPath); altJSONPath = path.join(__dirname, altJSONPath); - audioFilePath = path.join(__dirname, audioFilePath); let jsonExists = await fs.pathExists(jsonPath); let thumbnailExists = await fs.pathExists(thumbnailPath); @@ -918,7 +922,7 @@ async function deleteAudioFile(name, customPath = null, blacklistMode = false) { } } - let audioFileExists = await fs.pathExists(audioFilePath); + let fileExists = await fs.pathExists(file_obj.path); if (config_api.descriptors[name]) { try { @@ -932,18 +936,18 @@ async function deleteAudioFile(name, customPath = null, blacklistMode = false) { let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); if (useYoutubeDLArchive) { - const archive_path = path.join(archivePath, 'archive_audio.txt'); + const archive_path = path.join(archivePath, `archive_${type}.txt`); // get ID from JSON - var jsonobj = await utils.getJSONMp3(name, filePath); + var jsonobj = await (type === 'audio' ? utils.getJSONMp3(name, folderPath) : utils.getJSONMp4(name, folderPath)); let id = null; if (jsonobj) id = jsonobj.id; // use subscriptions API to remove video from the archive file, and write it to the blacklist if (await fs.pathExists(archive_path)) { const line = id ? await subscriptions_api.removeIDFromArchive(archive_path, id) : null; - if (blacklistMode && line) await writeToBlacklist('audio', line); + if (blacklistMode && line) await writeToBlacklist(type, line); } else { logger.info('Could not find archive file for audio files. Creating...'); await fs.close(await fs.open(archive_path, 'w')); @@ -952,84 +956,9 @@ async function deleteAudioFile(name, customPath = null, blacklistMode = false) { if (jsonExists) await fs.unlink(jsonPath); if (thumbnailExists) await fs.unlink(thumbnailPath); - if (audioFileExists) { - await fs.unlink(audioFilePath); - if (await fs.pathExists(jsonPath) || await fs.pathExists(audioFilePath)) { - return false; - } else { - return true; - } - } else { - // TODO: tell user that the file didn't exist - return true; - } -} - -async function deleteVideoFile(name, customPath = null, blacklistMode = false) { - let filePath = customPath ? customPath : videoFolderPath; - var jsonPath = path.join(filePath,name+'.info.json'); - - var altJSONPath = path.join(filePath,name+'.mp4.info.json'); - var videoFilePath = path.join(filePath,name+'.mp4'); - var thumbnailPath = path.join(filePath,name+'.webp'); - var altThumbnailPath = path.join(filePath,name+'.jpg'); - - jsonPath = path.join(__dirname, jsonPath); - videoFilePath = path.join(__dirname, videoFilePath); - - let jsonExists = await fs.pathExists(jsonPath); - let videoFileExists = await fs.pathExists(videoFilePath); - let thumbnailExists = await fs.pathExists(thumbnailPath); - - if (!jsonExists) { - if (await fs.pathExists(altJSONPath)) { - jsonExists = true; - jsonPath = altJSONPath; - } - } - - if (!thumbnailExists) { - if (await fs.pathExists(altThumbnailPath)) { - thumbnailExists = true; - thumbnailPath = altThumbnailPath; - } - } - - if (config_api.descriptors[name]) { - try { - for (let i = 0; i < config_api.descriptors[name].length; i++) { - config_api.descriptors[name][i].destroy(); - } - } catch(e) { - - } - } - - let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); - if (useYoutubeDLArchive) { - const archive_path = path.join(archivePath, 'archive_video.txt'); - - // get ID from JSON - - var jsonobj = await utils.getJSONMp4(name, filePath); - let id = null; - if (jsonobj) id = jsonobj.id; - - // use subscriptions API to remove video from the archive file, and write it to the blacklist - if (await fs.pathExists(archive_path)) { - const line = id ? await subscriptions_api.removeIDFromArchive(archive_path, id) : null; - if (blacklistMode && line) await writeToBlacklist('video', line); - } else { - logger.info('Could not find archive file for videos. Creating...'); - fs.closeSync(fs.openSync(archive_path, 'w')); - } - } - - if (jsonExists) await fs.unlink(jsonPath); - if (thumbnailExists) await fs.unlink(thumbnailPath); - if (videoFileExists) { - await fs.unlink(videoFilePath); - if (await fs.pathExists(jsonPath) || await fs.pathExists(videoFilePath)) { + if (fileExists) { + await fs.unlink(file_obj.path); + if (await fs.pathExists(jsonPath) || await fs.pathExists(file_obj.path)) { return false; } else { return true; @@ -1638,6 +1567,8 @@ async function cropFile(file_path, start, end, ext) { async function writeToBlacklist(type, line) { let blacklistPath = path.join(archivePath, (type === 'audio') ? 'blacklist_audio.txt' : 'blacklist_video.txt'); // adds newline to the beginning of the line + line.replace('\n', ''); + line.replace('\r', ''); line = '\n' + line; await fs.appendFile(blacklistPath, line); } @@ -2668,9 +2599,8 @@ app.post('/api/deletePlaylist', optionalJwt, async (req, res) => { // deletes non-subscription files app.post('/api/deleteFile', optionalJwt, async (req, res) => { - var uid = req.body.uid; - var type = req.body.type; - var blacklistMode = req.body.blacklistMode; + const uid = req.body.uid; + const blacklistMode = req.body.blacklistMode; if (req.isAuthenticated()) { let success = await auth_api.deleteUserFile(req.user.uid, uid, blacklistMode); @@ -2678,24 +2608,10 @@ app.post('/api/deleteFile', optionalJwt, async (req, res) => { return; } - var file_obj = db.get(`files`).find({uid: uid}).value(); - var name = file_obj.id; - var fullpath = file_obj ? file_obj.path : null; - var wasDeleted = false; - if (await fs.pathExists(fullpath)) - { - wasDeleted = type === 'audio' ? await deleteAudioFile(name, path.dirname(fullpath), blacklistMode) : await deleteVideoFile(name, path.dirname(fullpath), blacklistMode); - db.get('files').remove({uid: uid}).write(); - wasDeleted = true; - res.send(wasDeleted); - } else if (file_obj) { - db.get('files').remove({uid: uid}).write(); - wasDeleted = true; - res.send(wasDeleted); - } else { - wasDeleted = false; - res.send(wasDeleted); - } + let wasDeleted = false; + wasDeleted = await deleteFile(uid, null, blacklistMode); + db.get('files').remove({uid: uid}).write(); + res.send(wasDeleted); }); app.post('/api/downloadFile', optionalJwt, async (req, res) => { @@ -2805,6 +2721,8 @@ app.post('/api/generateNewAPIKey', function (req, res) { app.get('/api/stream', optionalJwt, async (req, res) => { const type = req.query.type; + const uuid = req.query.uuid ? req.query.uuid : (req.user ? req.user.uid : null); + const sub_id = req.query.sub_id; const ext = type === 'audio' ? '.mp3' : '.mp4'; const mimetype = type === 'audio' ? 'audio/mp3' : 'video/mp4'; var head; @@ -2815,7 +2733,7 @@ app.get('/api/stream', optionalJwt, async (req, res) => { const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode'); if (!multiUserMode || req.isAuthenticated() || req.can_watch) { - const file_obj = await db_api.getVideo(uid, req.query.uuid ? req.query.uuid : (req.user ? req.user.uid : null), req.query.sub_id); + const file_obj = await db_api.getVideo(uid, uuid, sub_id); if (file_obj) file_path = file_obj['path']; else file_path = null; } diff --git a/src/app/components/recent-videos/recent-videos.component.ts b/src/app/components/recent-videos/recent-videos.component.ts index 2b8fe94..31ed771 100644 --- a/src/app/components/recent-videos/recent-videos.component.ts +++ b/src/app/components/recent-videos/recent-videos.component.ts @@ -244,7 +244,7 @@ export class RecentVideosComponent implements OnInit { } deleteNormalFile(file, blacklistMode = false) { - this.postsService.deleteFile(file.uid, file.isAudio ? 'audio' : 'video', blacklistMode).subscribe(result => { + this.postsService.deleteFile(file.uid, blacklistMode).subscribe(result => { if (result) { this.postsService.openSnackBar('Delete success!', 'OK.'); this.removeFileCard(file); diff --git a/src/app/file-card/file-card.component.ts b/src/app/file-card/file-card.component.ts index 68a8453..90b906c 100644 --- a/src/app/file-card/file-card.component.ts +++ b/src/app/file-card/file-card.component.ts @@ -56,7 +56,7 @@ export class FileCardComponent implements OnInit { deleteFile(blacklistMode = false) { if (!this.playlist) { - this.postsService.deleteFile(this.uid, this.isAudio ? 'audio' : 'video', blacklistMode).subscribe(result => { + this.postsService.deleteFile(this.uid, blacklistMode).subscribe(result => { if (result) { this.openSnackBar('Delete success!', 'OK.'); this.removeFile.emit(this.name); From a11445b80db7db8a4fb3f75c7ca5a10654dfc196 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 16 May 2021 02:54:15 -0600 Subject: [PATCH 059/680] Added backend tests and made authentication more testable --- backend/authentication/auth.js | 14 +- backend/package-lock.json | 1159 ++++++++++++++++++++++---------- backend/package.json | 1 + backend/test/tests.js | 94 +++ 4 files changed, 912 insertions(+), 356 deletions(-) create mode 100644 backend/test/tests.js diff --git a/backend/authentication/auth.js b/backend/authentication/auth.js index e7cf337..3af31e7 100644 --- a/backend/authentication/auth.js +++ b/backend/authentication/auth.js @@ -144,16 +144,18 @@ exports.registerUser = function(req, res) { ************************************************/ +exports.login = async (username, password) => { + const user = users_db.get('users').find({name: username}).value(); + if (!user) { logger.error(`User ${username} not found`); false } + if (user.auth_method && user.auth_method !== 'internal') { return false } + return await bcrypt.compare(password, user.passhash) ? user : false; +} + exports.passport.use(new LocalStrategy({ usernameField: 'username', passwordField: 'password'}, async function(username, password, done) { - const user = users_db.get('users').find({name: username}).value(); - if (!user) { logger.error(`User ${username} not found`); return done(null, false); } - if (user.auth_method && user.auth_method !== 'internal') { return done(null, false); } - if (user) { - return done(null, (await bcrypt.compare(password, user.passhash)) ? user : false); - } + return done(null, await exports.login(username, password)); } )); diff --git a/backend/package-lock.json b/backend/package-lock.json index 4067dd9..74f76fa 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -4,6 +4,19 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "requires": { + "defer-to-connect": "^1.0.1" + } + }, "@types/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", @@ -87,6 +100,11 @@ "@types/mime": "*" } }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==" + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -113,24 +131,56 @@ } }, "ansi-align": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", + "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", "requires": { - "string-width": "^2.0.0" + "string-width": "^3.0.0" + }, + "dependencies": { + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } } }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==" + }, "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "requires": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" + }, + "dependencies": { + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + } } }, "any-promise": { @@ -209,6 +259,11 @@ } } }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -306,9 +361,9 @@ } }, "binary-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", - "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, "bl": { "version": "4.0.2", @@ -350,17 +405,18 @@ } }, "boxen": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", - "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", + "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", "requires": { - "ansi-align": "^2.0.0", - "camelcase": "^4.0.0", - "chalk": "^2.0.1", - "cli-boxes": "^1.0.0", - "string-width": "^2.0.0", - "term-size": "^1.2.0", - "widest-line": "^2.0.0" + "ansi-align": "^3.0.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "cli-boxes": "^2.2.0", + "string-width": "^4.1.0", + "term-size": "^2.1.0", + "type-fest": "^0.8.1", + "widest-line": "^3.1.0" } }, "brace-expansion": { @@ -380,6 +436,11 @@ "fill-range": "^7.0.1" } }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" + }, "buffer": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.5.0.tgz", @@ -462,15 +523,31 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + } + } }, - "capture-stack-trace": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", - "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==" + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, "caseless": { "version": "0.12.0", @@ -486,13 +563,27 @@ } }, "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } } }, "charenc": { @@ -501,29 +592,62 @@ "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" }, "chokidar": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz", - "integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", "requires": { "anymatch": "~3.1.1", "braces": "~3.0.2", - "fsevents": "~2.1.2", + "fsevents": "~2.3.1", "glob-parent": "~5.1.0", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", - "readdirp": "~3.3.0" + "readdirp": "~3.5.0" } }, "ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" }, "cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=" + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==" + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + } }, "color": { "version": "3.0.0", @@ -673,16 +797,16 @@ } }, "configstore": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", - "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", "requires": { - "dot-prop": "^4.1.0", + "dot-prop": "^5.2.0", "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "unique-string": "^1.0.0", - "write-file-atomic": "^2.0.0", - "xdg-basedir": "^3.0.0" + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" } }, "connected-domain": { @@ -735,14 +859,6 @@ "readable-stream": "^3.4.0" } }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "requires": { - "capture-stack-trace": "^1.0.0" - } - }, "cross-spawn": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", @@ -759,9 +875,9 @@ "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" }, "crypto-random-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" }, "dashdash": { "version": "1.14.1", @@ -779,11 +895,29 @@ "ms": "2.0.0" } }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==" + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "^1.0.0" + } + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -841,12 +975,17 @@ } } }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==" + }, "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", "requires": { - "is-obj": "^1.0.0" + "is-obj": "^2.0.0" } }, "dtrace-provider": { @@ -909,6 +1048,11 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, "enabled": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", @@ -935,15 +1079,25 @@ "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz", "integrity": "sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg==" }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==" + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" }, "etag": { "version": "1.8.1", @@ -1061,6 +1215,20 @@ "unpipe": "~1.0.0" } }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==" + }, "fluent-ffmpeg": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz", @@ -1132,9 +1300,9 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", - "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "optional": true }, "fstream": { @@ -1148,6 +1316,11 @@ "rimraf": "2" } }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, "get-stream": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", @@ -1178,48 +1351,46 @@ } }, "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "requires": { "is-glob": "^4.0.1" } }, "global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", + "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", "requires": { - "ini": "^1.3.4" + "ini": "1.3.7" } }, "got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", "requires": { - "create-error-class": "^3.0.0", + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" }, "dependencies": { "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } } } }, @@ -1228,6 +1399,11 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==" + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -1247,6 +1423,11 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, + "has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==" + }, "hashish": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/hashish/-/hashish-0.0.4.tgz", @@ -1255,6 +1436,11 @@ "traverse": ">=0.2.4" } }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, "hh-mm-ss": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hh-mm-ss/-/hh-mm-ss-1.2.0.tgz", @@ -1263,6 +1449,11 @@ "zero-fill": "^2.2.3" } }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -1340,9 +1531,9 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", + "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==" }, "ipaddr.js": { "version": "1.9.1", @@ -1368,11 +1559,11 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", "requires": { - "ci-info": "^1.5.0" + "ci-info": "^2.0.0" } }, "is-extglob": { @@ -1394,18 +1585,18 @@ } }, "is-installed-globally": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", - "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", + "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", "requires": { - "global-dirs": "^0.1.0", - "is-path-inside": "^1.0.0" + "global-dirs": "^2.0.1", + "is-path-inside": "^3.0.1" } }, "is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", + "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==" }, "is-number": { "version": "7.0.0", @@ -1413,33 +1604,25 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" }, "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "requires": { - "path-is-inside": "^1.0.1" - } + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==" + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==" }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" }, - "is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" - }, - "is-retry-allowed": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", - "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==" - }, "is-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", @@ -1450,6 +1633,11 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, + "is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -1465,11 +1653,24 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, + "js-yaml": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz", + "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==", + "requires": { + "argparse": "^2.0.1" + } + }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -1556,6 +1757,14 @@ "safe-buffer": "^5.0.1" } }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "requires": { + "json-buffer": "3.0.0" + } + }, "kuler": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", @@ -1565,11 +1774,11 @@ } }, "latest-version": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", "requires": { - "package-json": "^4.0.0" + "package-json": "^6.3.0" } }, "lazystream": { @@ -1667,6 +1876,14 @@ "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=" }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "requires": { + "p-locate": "^5.0.0" + } + }, "lodash": { "version": "4.17.19", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", @@ -1727,6 +1944,38 @@ "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" }, + "log-symbols": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "requires": { + "chalk": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "logform": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", @@ -1763,21 +2012,19 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "requires": { - "pify": "^3.0.0" + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } } }, "md5": { @@ -1841,6 +2088,11 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -1862,6 +2114,83 @@ "minimist": "^1.2.5" } }, + "mocha": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz", + "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==", + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.1", + "debug": "4.3.1", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.6", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.0.0", + "log-symbols": "4.0.0", + "minimatch": "3.0.4", + "ms": "2.1.3", + "nanoid": "3.1.20", + "serialize-javascript": "5.0.1", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.1.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "nanoid": { + "version": "3.1.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", + "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==" + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "moment": { "version": "2.29.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", @@ -1993,9 +2322,9 @@ } }, "nodemon": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.2.tgz", - "integrity": "sha512-GWhYPMfde2+M0FsHnggIHXTqPDHXia32HRhh6H0d75Mt9FKUoCBvumNHr7LdrpPBTKxsWmIEOjoN+P4IU6Hcaw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz", + "integrity": "sha512-XHzK69Awgnec9UzHr1kc8EomQh4sjTQ8oRf8TsGrSmHDx9/UmiGG9E/mM3BuTfNeFwdNBvrqQq/RHL0xIeyFOA==", "requires": { "chokidar": "^3.2.2", "debug": "^3.2.6", @@ -2005,22 +2334,22 @@ "semver": "^5.7.1", "supports-color": "^5.5.0", "touch": "^3.1.0", - "undefsafe": "^2.0.2", - "update-notifier": "^2.5.0" + "undefsafe": "^2.0.3", + "update-notifier": "^4.1.0" }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "requires": { "ms": "^2.1.1" } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" } } }, @@ -2037,6 +2366,11 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" + }, "npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -2089,20 +2423,48 @@ "mimic-fn": "^2.1.0" } }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" + }, "p-finally": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==" }, - "package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "requires": { - "got": "^6.7.1", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "requires": { + "p-limit": "^3.0.2" + } + }, + "package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "requires": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } } }, "parseurl": { @@ -2184,16 +2546,16 @@ "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" - }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2230,9 +2592,9 @@ "integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw=" }, "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" }, "process-nextick-args": { "version": "2.0.1", @@ -2261,20 +2623,15 @@ "table-parser": "^0.1.3" } }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, "pstree.remy": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.7.tgz", - "integrity": "sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A==" + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" }, "pump": { "version": "3.0.0", @@ -2290,11 +2647,27 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, + "pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "requires": { + "escape-goat": "^2.0.0" + } + }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -2348,28 +2721,27 @@ } }, "readdirp": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz", - "integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", "requires": { - "picomatch": "^2.0.7" + "picomatch": "^2.2.1" } }, "registry-auth-token": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", - "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", + "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==", "requires": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" + "rc": "^1.2.8" } }, "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", "requires": { - "rc": "^1.0.1" + "rc": "^1.2.8" } }, "request": { @@ -2411,6 +2783,19 @@ } } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "requires": { + "lowercase-keys": "^1.0.0" + } + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -2441,11 +2826,18 @@ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", "requires": { - "semver": "^5.0.3" + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } } }, "send": { @@ -2475,6 +2867,14 @@ } } }, + "serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "requires": { + "randombytes": "^2.1.0" + } + }, "serve-static": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", @@ -2578,12 +2978,38 @@ "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + } } }, "string_decoder": { @@ -2595,18 +3021,13 @@ } }, "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^4.1.0" } }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" - }, "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -2646,87 +3067,9 @@ } }, "term-size": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", - "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", - "requires": { - "execa": "^0.7.0" - }, - "dependencies": { - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "requires": { - "path-key": "^2.0.0" - } - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "^2.0.0" - } - } - } + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==" }, "text-hex": { "version": "1.0.0", @@ -2749,10 +3092,10 @@ "thenify": ">= 3.1.0 < 4" } }, - "timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" }, "to-regex-range": { "version": "5.0.1", @@ -2807,6 +3150,11 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -2821,6 +3169,14 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, "undefsafe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", @@ -2830,11 +3186,11 @@ } }, "unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", "requires": { - "crypto-random-string": "^1.0.0" + "crypto-random-string": "^2.0.0" } }, "universalify": { @@ -2847,11 +3203,6 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, - "unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=" - }, "unzipper": { "version": "0.10.10", "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.10.tgz", @@ -2886,20 +3237,23 @@ } }, "update-notifier": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", + "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", "requires": { - "boxen": "^1.2.1", - "chalk": "^2.0.1", - "configstore": "^3.0.0", + "boxen": "^4.2.0", + "chalk": "^3.0.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", "import-lazy": "^2.1.0", - "is-ci": "^1.0.10", - "is-installed-globally": "^0.1.0", - "is-npm": "^1.0.0", - "latest-version": "^3.0.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^3.0.0" + "is-ci": "^2.0.0", + "is-installed-globally": "^0.3.1", + "is-npm": "^4.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.0.0", + "pupa": "^2.0.1", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" } }, "uri-js": { @@ -2911,11 +3265,11 @@ } }, "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", "requires": { - "prepend-http": "^1.0.1" + "prepend-http": "^2.0.0" } }, "util-deprecate": { @@ -2987,12 +3341,44 @@ "isexe": "^2.0.0" } }, - "widest-line": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", - "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "requires": { - "string-width": "^2.1.1" + "string-width": "^1.0.2 || 2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "requires": { + "string-width": "^4.0.0" } }, "winston": { @@ -3051,35 +3437,108 @@ } } }, + "workerpool": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.0.tgz", + "integrity": "sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==" + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write-file-atomic": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", "requires": { - "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" } }, "xdg-basedir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==" + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==" + } + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" }, "youtube-dl": { "version": "3.0.2", diff --git a/backend/package.json b/backend/package.json index a38315d..72607de 100644 --- a/backend/package.json +++ b/backend/package.json @@ -44,6 +44,7 @@ "lowdb": "^1.0.0", "md5": "^2.2.1", "merge-files": "^0.1.2", + "mocha": "^8.4.0", "moment": "^2.29.1", "multer": "^1.4.2", "node-fetch": "^2.6.1", diff --git a/backend/test/tests.js b/backend/test/tests.js new file mode 100644 index 0000000..c9726a0 --- /dev/null +++ b/backend/test/tests.js @@ -0,0 +1,94 @@ +var assert = require('assert'); +const low = require('lowdb') +var winston = require('winston'); + +process.chdir('./backend') + +const FileSync = require('lowdb/adapters/FileSync'); + +const adapter = new FileSync('./appdata/db.json'); +const db = low(adapter) + +const users_adapter = new FileSync('./appdata/users.json'); +const users_db = low(users_adapter); + +const defaultFormat = winston.format.printf(({ level, message, label, timestamp }) => { + return `${timestamp} ${level.toUpperCase()}: ${message}`; +}); + +let debugMode = process.env.YTDL_MODE === 'debug'; + +const logger = winston.createLogger({ + level: 'info', + format: winston.format.combine(winston.format.timestamp(), defaultFormat), + defaultMeta: {}, + transports: [ + // + // - Write to all logs with level `info` and below to `combined.log` + // - Write all logs error (and below) to `error.log`. + // + new winston.transports.File({ filename: 'appdata/logs/error.log', level: 'error' }), + new winston.transports.File({ filename: 'appdata/logs/combined.log' }), + new winston.transports.Console({level: !debugMode ? 'info' : 'debug', name: 'console'}) + ] +}); + +var auth_api = require('../authentication/auth'); +var db_api = require('../db'); + +db_api.initialize(db, users_db, logger); +auth_api.initialize(db, users_db, logger); + +describe('Multi User', async function() { + let user = null; + const user_to_test = 'admin'; + before(async function() { + user = await auth_api.login('admin', 'pass'); + console.log('hi') + }); + describe('Authentication', function() { + it('login', async function() { + assert(user); + }); + }); + describe('Video player - normal', function() { + const video_to_test = 'ebbcfffb-d6f1-4510-ad25-d1ec82e0477e'; + it('Get video', async function() { + const video_obj = db_api.getVideo(video_to_test, 'admin'); + assert(video_obj); + }); + + it('Video access - disallowed', async function() { + await db_api.setVideoProperty(video_to_test, {sharingEnabled: false}, user_to_test); + const video_obj = auth_api.getUserVideo('admin', video_to_test, true); + assert(!video_obj); + }); + + it('Video access - allowed', async function() { + await db_api.setVideoProperty(video_to_test, {sharingEnabled: true}, user_to_test); + const video_obj = auth_api.getUserVideo('admin', video_to_test, true); + assert(video_obj); + }); + }); + // describe('Video player - subscription', function() { + // const sub_to_test = ''; + // const video_to_test = 'ebbcfffb-d6f1-4510-ad25-d1ec82e0477e'; + // it('Get video', async function() { + // const video_obj = db_api.getVideo(video_to_test, 'admin', ); + // assert(video_obj); + // }); + + // it('Video access - disallowed', async function() { + // await db_api.setVideoProperty(video_to_test, {sharingEnabled: false}, user_to_test, sub_to_test); + // const video_obj = auth_api.getUserVideo('admin', video_to_test, true); + // assert(!video_obj); + // }); + + // it('Video access - allowed', async function() { + // await db_api.setVideoProperty(video_to_test, {sharingEnabled: true}, user_to_test, sub_to_test); + // const video_obj = auth_api.getUserVideo('admin', video_to_test, true); + // assert(video_obj); + // }); + // }); + +}); \ No newline at end of file From 07b48a4da154094aef63b50d5e59d9733f28e3e0 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 16 May 2021 02:55:27 -0600 Subject: [PATCH 060/680] Fixed backend security issues with several dependencies --- backend/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 74f76fa..e7edcd2 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -366,9 +366,9 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, "bl": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", - "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "requires": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -1885,9 +1885,9 @@ } }, "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.defaults": { "version": "4.2.0", From 419fe3c3c6a0739fdfc7cc7d3805a987aa26ca4c Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 16 May 2021 02:58:16 -0600 Subject: [PATCH 061/680] Fixed frontend security issues for several depepndencies --- package-lock.json | 316 +++++++++++++++++++--------------------------- 1 file changed, 131 insertions(+), 185 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7ca348b..61b14b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -180,9 +180,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "semver": { @@ -316,6 +316,12 @@ "ms": "2.1.2" } }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, "resolve": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz", @@ -432,9 +438,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "semver": { @@ -705,9 +711,9 @@ }, "dependencies": { "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true } } @@ -784,9 +790,9 @@ }, "dependencies": { "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true } } @@ -1592,9 +1598,9 @@ }, "dependencies": { "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" } } }, @@ -1609,9 +1615,9 @@ }, "dependencies": { "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" } } }, @@ -1760,6 +1766,12 @@ "semver-intersect": "1.4.0" }, "dependencies": { + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, "semver": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", @@ -2616,15 +2628,6 @@ "tweetnacl": "^0.14.3" } }, - "better-assert": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", - "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", - "dev": true, - "requires": { - "callsite": "1.0.0" - } - }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -3059,12 +3062,6 @@ "caller-callsite": "^2.0.0" } }, - "callsite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", - "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", - "dev": true - }, "callsites": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", @@ -4513,24 +4510,24 @@ "dev": true }, "elliptic": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", - "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", "dev": true, "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", + "bn.js": "^4.11.9", + "brorand": "^1.1.0", "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" }, "dependencies": { "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -4582,37 +4579,37 @@ } }, "engine.io": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.2.tgz", - "integrity": "sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.5.0.tgz", + "integrity": "sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==", "dev": true, "requires": { "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "0.3.1", + "cookie": "~0.4.1", "debug": "~4.1.0", "engine.io-parser": "~2.2.0", - "ws": "^7.1.2" + "ws": "~7.4.2" }, "dependencies": { "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", "dev": true }, "ws": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz", - "integrity": "sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ==", + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", + "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", "dev": true } } }, "engine.io-client": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.4.tgz", - "integrity": "sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.2.tgz", + "integrity": "sha512-QEqIp+gJ/kMHeUun7f5Vv3bteRHppHH/FMBQX/esFj/fuYfjyUKWGMo3VCvIP/V8bE9KcjHmRZrhIz2Z9oNsDA==", "dev": true, "requires": { "component-emitter": "~1.3.0", @@ -4623,8 +4620,8 @@ "indexof": "0.0.1", "parseqs": "0.0.6", "parseuri": "0.0.6", - "ws": "~6.1.0", - "xmlhttprequest-ssl": "~1.5.4", + "ws": "~7.4.2", + "xmlhttprequest-ssl": "~1.6.2", "yeast": "0.1.2" }, "dependencies": { @@ -4643,26 +4640,11 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, - "parseqs": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", - "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==", - "dev": true - }, - "parseuri": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", - "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==", - "dev": true - }, "ws": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", - "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0" - } + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", + "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", + "dev": true } } }, @@ -5930,9 +5912,9 @@ } }, "hosted-git-info": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.7.tgz", - "integrity": "sha512-fWqc0IcuXs+BmE9orLDyVykAG9GJtGLGuZAAqgcckPgv5xad4AcXGIv8galtQvlwutxSlaMcdw7BUtq2EIvqCQ==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz", + "integrity": "sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -6338,9 +6320,9 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, "inquirer": { @@ -6405,9 +6387,9 @@ "dev": true }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "supports-color": { @@ -7498,9 +7480,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.memoize": { "version": "4.1.2", @@ -7719,9 +7701,9 @@ } }, "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", + "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", "dev": true, "requires": { "figgy-pudding": "^3.5.1" @@ -8289,9 +8271,9 @@ }, "dependencies": { "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true } } @@ -8440,9 +8422,9 @@ }, "dependencies": { "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, "lru-cache": { @@ -8516,12 +8498,6 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true }, - "object-component": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", - "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", - "dev": true - }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -8942,9 +8918,9 @@ } }, "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, "lru-cache": { @@ -9008,9 +8984,9 @@ } }, "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", + "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", "dev": true, "requires": { "figgy-pudding": "^3.5.1" @@ -9144,22 +9120,16 @@ } }, "parseqs": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", - "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", - "dev": true, - "requires": { - "better-assert": "~1.0.0" - } + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==", + "dev": true }, "parseuri": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", - "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", - "dev": true, - "requires": { - "better-assert": "~1.0.0" - } + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==", + "dev": true }, "parseurl": { "version": "1.3.3", @@ -11677,16 +11647,16 @@ } }, "socket.io": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", - "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.4.1.tgz", + "integrity": "sha512-Si18v0mMXGAqLqCVpTxBa8MGqriHGQh8ccEOhmsmNS3thNCGBwO8WGrwMibANsWtQQ5NStdZwHqZR3naJVFc3w==", "dev": true, "requires": { "debug": "~4.1.0", - "engine.io": "~3.4.0", + "engine.io": "~3.5.0", "has-binary2": "~1.0.2", "socket.io-adapter": "~1.1.0", - "socket.io-client": "2.3.0", + "socket.io-client": "2.4.0", "socket.io-parser": "~3.4.0" } }, @@ -11697,38 +11667,32 @@ "dev": true }, "socket.io-client": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", - "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz", + "integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==", "dev": true, "requires": { "backo2": "1.0.2", - "base64-arraybuffer": "0.1.5", "component-bind": "1.0.0", - "component-emitter": "1.2.1", - "debug": "~4.1.0", - "engine.io-client": "~3.4.0", + "component-emitter": "~1.3.0", + "debug": "~3.1.0", + "engine.io-client": "~3.5.0", "has-binary2": "~1.0.2", - "has-cors": "1.1.0", "indexof": "0.0.1", - "object-component": "0.0.3", - "parseqs": "0.0.5", - "parseuri": "0.0.5", + "parseqs": "0.0.6", + "parseuri": "0.0.6", "socket.io-parser": "~3.3.0", "to-array": "0.1.4" }, "dependencies": { - "base64-arraybuffer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", - "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", - "dev": true - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } }, "isarray": { "version": "2.0.1", @@ -11743,31 +11707,14 @@ "dev": true }, "socket.io-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz", - "integrity": "sha512-1QLvVAe8dTz+mKmZ07Swxt+LAo4Y1ff50rlyoEx00TQmDFVQYPfcqGvIDJLGaBdhdNCecXtyKpD+EgKGcmmbuQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz", + "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==", "dev": true, "requires": { "component-emitter": "~1.3.0", "debug": "~3.1.0", "isarray": "2.0.1" - }, - "dependencies": { - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } } } } @@ -12082,9 +12029,9 @@ } }, "ssri": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz", - "integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", "dev": true, "requires": { "minipass": "^3.1.1" @@ -13074,9 +13021,9 @@ } }, "url-parse": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", - "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz", + "integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==", "dev": true, "requires": { "querystringify": "^2.1.1", @@ -13796,8 +13743,7 @@ }, "ssri": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "resolved": "", "dev": true, "requires": { "figgy-pudding": "^3.5.1" @@ -14538,9 +14484,9 @@ "dev": true }, "xmlhttprequest-ssl": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", - "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.2.tgz", + "integrity": "sha512-tYOaldF/0BLfKuoA39QMwD4j2m8lq4DIncqj1yuNELX4vz9+z/ieG/vwmctjJce+boFHXstqhWnHSxc4W8f4qg==", "dev": true }, "xtend": { From b933af03e2f1eaf7825e9d9a28cedd34164ce785 Mon Sep 17 00:00:00 2001 From: Erwan Date: Sat, 22 May 2021 14:58:48 +0200 Subject: [PATCH 062/680] Update API docs links in settings --- src/app/settings/settings.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/settings/settings.component.html b/src/app/settings/settings.component.html index 346797c..10a7613 100644 --- a/src/app/settings/settings.component.html +++ b/src/app/settings/settings.component.html @@ -219,7 +219,7 @@
From e2c31319cf236bff1d5d1b26b74d101112f5f1b9 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 23 May 2021 03:59:38 -0600 Subject: [PATCH 063/680] Migrated playlist and subscription (per video and sub-wide) video downloading functionality to new schema Migrated modify playlist component to new schema Moved wait function and playlist generation function(s) to utils - added tests for zip generation --- backend/app.js | 95 ++++++------------- backend/db.js | 1 - backend/test/tests.js | 36 +++++++ backend/utils.js | 54 ++++++++++- .../custom-playlists.component.ts | 2 +- .../create-playlist.component.html | 4 +- .../modify-playlist.component.html | 54 ++++++----- .../modify-playlist.component.ts | 30 +++--- src/app/file-card/file-card.component.ts | 2 +- src/app/main/main.component.ts | 2 +- src/app/player/player.component.ts | 5 +- src/app/posts.services.ts | 18 +++- .../subscription/subscription.component.ts | 5 +- 13 files changed, 189 insertions(+), 119 deletions(-) diff --git a/backend/app.js b/backend/app.js index 8ae57ac..065b339 100644 --- a/backend/app.js +++ b/backend/app.js @@ -193,16 +193,6 @@ app.use(auth_api.passport.initialize()); // actual functions -/** - * setTimeout, but its a promise. - * @param {number} ms - */ -async function wait(ms) { - await new Promise(resolve => { - setTimeout(resolve, ms); - }); -} - async function checkMigrations() { // 3.5->3.6 migration const files_to_db_migration_complete = true; // migration phased out! previous code: db.get('files_to_db_migration_complete').value(); @@ -529,7 +519,7 @@ async function backupServerLite() { }); // wait a tiny bit for the zip to reload in fs - await wait(100); + await utils.wait(100); return true; } @@ -597,7 +587,7 @@ async function killAllDownloads() { async function setPortItemFromENV() { config_api.setConfigItem('ytdl_port', backendPort.toString()); - await wait(100); + await utils.wait(100); return true; } @@ -611,7 +601,7 @@ async function setConfigFromEnv() { let success = config_api.setConfigItems(config_items); if (success) { logger.info('Config items set using ENV variables.'); - await wait(100); + await utils.wait(100); return true; } else { logger.error('ERROR: Failed to set config items using ENV variables.'); @@ -847,47 +837,6 @@ function getVideoFormatID(name) } } -async function createPlaylistZipFile(fileNames, type, outputName, fullPathProvided = null, user_uid = null) { - let zipFolderPath = null; - - if (!fullPathProvided) { - zipFolderPath = (type === 'audio') ? audioFolderPath : videoFolderPath - if (user_uid) zipFolderPath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, zipFolderPath); - } else { - zipFolderPath = path.join(__dirname, config_api.getConfigItem('ytdl_subscriptions_base_path')); - } - - let ext = (type === 'audio') ? '.mp3' : '.mp4'; - - let output = fs.createWriteStream(path.join(zipFolderPath, outputName + '.zip')); - - var archive = archiver('zip', { - gzip: true, - zlib: { level: 9 } // Sets the compression level. - }); - - archive.on('error', function(err) { - logger.error(err); - throw err; - }); - - // pipe archive data to the output file - archive.pipe(output); - - for (let i = 0; i < fileNames.length; i++) { - let fileName = fileNames[i]; - let fileNamePathRemoved = path.parse(fileName).base; - let file_path = !fullPathProvided ? path.join(zipFolderPath, fileName + ext) : fileName; - archive.file(file_path, {name: fileNamePathRemoved + ext}) - } - - await archive.finalize(); - - // wait a tiny bit for the zip to reload in fs - await wait(100); - return path.join(zipFolderPath,outputName + '.zip'); -} - // TODO: add to db_api and support multi-user mode async function deleteFile(uid, uuid = null, blacklistMode = false) { const file_obj = await db_api.getVideo(uid, uuid); @@ -2523,18 +2472,19 @@ app.post('/api/getPlaylist', optionalJwt, async (req, res) => { let include_file_metadata = req.body.include_file_metadata; const playlist = await db_api.getPlaylist(playlist_id, uuid); + const file_objs = []; if (playlist && include_file_metadata) { - playlist['file_objs'] = []; for (let i = 0; i < playlist['uids'].length; i++) { const uid = playlist['uids'][i]; const file_obj = await db_api.getVideo(uid, uuid); - playlist['file_objs'].push(file_obj); + file_objs.push(file_obj); } } res.send({ playlist: playlist, + file_objs: file_objs, type: playlist && playlist.type, success: !!playlist }); @@ -2616,32 +2566,47 @@ app.post('/api/deleteFile', optionalJwt, async (req, res) => { app.post('/api/downloadFile', optionalJwt, async (req, res) => { let uid = req.body.uid; - let is_playlist = req.body.is_playlist; let uuid = req.body.uuid; + let playlist_id = req.body.playlist_id; + let sub_id = req.body.sub_id; let file_path_to_download = null; if (!uuid && req.user) uuid = req.user.uid; - if (is_playlist) { + + let zip_file_generated = false; + if (playlist_id) { + zip_file_generated = true; const playlist_files_to_download = []; - const playlist = db_api.getPlaylist(uid, uuid); + const playlist = await db_api.getPlaylist(playlist_id, uuid); for (let i = 0; i < playlist['uids'].length; i++) { - const uid = playlist['uids'][i]; - const file_obj = await db_api.getVideo(uid, uuid); - playlist_files_to_download.push(file_obj.path); + const playlist_file_uid = playlist['uids'][i]; + const file_obj = await db_api.getVideo(playlist_file_uid, uuid); + playlist_files_to_download.push(file_obj); } // generate zip - file_path_to_download = await createPlaylistZipFile(playlist_files_to_download, playlist.type, playlist.name); + file_path_to_download = await utils.createContainerZipFile(playlist, playlist_files_to_download); + } else if (sub_id && !uid) { + zip_file_generated = true; + const sub_files_to_download = []; + const sub = subscriptions_api.getSubscription(sub_id, uuid); + for (let i = 0; i < sub['videos'].length; i++) { + const sub_file = sub['videos'][i]; + sub_files_to_download.push(sub_file); + } + + // generate zip + file_path_to_download = await utils.createContainerZipFile(sub, sub_files_to_download); } else { - const file_obj = await db_api.getVideo(uid, uuid) + const file_obj = await db_api.getVideo(uid, uuid, sub_id) file_path_to_download = file_obj.path; } if (!path.isAbsolute(file_path_to_download)) file_path_to_download = path.join(__dirname, file_path_to_download); res.sendFile(file_path_to_download, function (err) { if (err) { logger.error(err); - } else if (is_playlist) { + } else if (zip_file_generated) { try { // delete generated zip file fs.unlinkSync(file_path_to_download); diff --git a/backend/db.js b/backend/db.js index 061280b..106c3f2 100644 --- a/backend/db.js +++ b/backend/db.js @@ -245,7 +245,6 @@ exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = fal if (uid) playlist['uids'].push(uid); else logger.warn(`Failed to convert file with name ${fileName} to its UID while converting playlist ${playlist['name']} to the new UID-based schema. The original file is likely missing/deleted and it will be skipped.`); } - delete playlist['fileNames']; exports.updatePlaylist(playlist, user_uid); } diff --git a/backend/test/tests.js b/backend/test/tests.js index c9726a0..9697a36 100644 --- a/backend/test/tests.js +++ b/backend/test/tests.js @@ -35,13 +35,19 @@ const logger = winston.createLogger({ var auth_api = require('../authentication/auth'); var db_api = require('../db'); +const utils = require('../utils'); +const subscriptions_api = require('../subscriptions'); +const fs = require('fs-extra'); db_api.initialize(db, users_db, logger); auth_api.initialize(db, users_db, logger); +subscriptions_api.initialize(db, users_db, logger, db_api); describe('Multi User', async function() { let user = null; const user_to_test = 'admin'; + const sub_to_test = 'dc834388-3454-41bf-a618-e11cb8c7de1c'; + const playlist_to_test = 'ysabVZz4x'; before(async function() { user = await auth_api.login('admin', 'pass'); console.log('hi') @@ -70,6 +76,36 @@ describe('Multi User', async function() { assert(video_obj); }); }); + describe('Zip generators', function() { + it('Playlist zip generator', async function() { + const playlist = await db_api.getPlaylist(playlist_to_test, user_to_test); + assert(playlist); + const playlist_files_to_download = []; + for (let i = 0; i < playlist['uids'].length; i++) { + const uid = playlist['uids'][i]; + const playlist_file = await db_api.getVideo(uid, user_to_test); + playlist_files_to_download.push(playlist_file); + } + const zip_path = await utils.createContainerZipFile(playlist, playlist_files_to_download); + const zip_exists = fs.pathExistsSync(zip_path); + assert(zip_exists); + if (zip_exists) fs.unlinkSync(zip_path); + }); + + it('Subscription zip generator', async function() { + const sub = subscriptions_api.getSubscription(sub_to_test, user_to_test); + assert(sub); + const sub_files_to_download = []; + for (let i = 0; i < sub['videos'].length; i++) { + const sub_file = sub['videos'][i]; + sub_files_to_download.push(sub_file); + } + const zip_path = await utils.createContainerZipFile(sub, sub_files_to_download); + const zip_exists = fs.pathExistsSync(zip_path); + assert(zip_exists); + if (zip_exists) fs.unlinkSync(zip_path); + }); + }); // describe('Video player - subscription', function() { // const sub_to_test = ''; // const video_to_test = 'ebbcfffb-d6f1-4510-ad25-d1ec82e0477e'; diff --git a/backend/utils.js b/backend/utils.js index cd7c23d..2b825dd 100644 --- a/backend/utils.js +++ b/backend/utils.js @@ -1,6 +1,7 @@ -var fs = require('fs-extra') -var path = require('path') +const fs = require('fs-extra') +const path = require('path') const config_api = require('./config'); +const archiver = require('archiver'); const is_windows = process.platform === 'win32'; @@ -52,6 +53,43 @@ async function getDownloadedFilesByType(basePath, type, full_metadata = false) { return files; } +async function createContainerZipFile(container_obj, container_file_objs) { + const container_files_to_download = []; + for (let i = 0; i < container_file_objs.length; i++) { + const container_file_obj = container_file_objs[i]; + container_files_to_download.push(container_file_obj.path); + } + return await createZipFile(path.join('appdata', container_obj.name + '.zip'), container_files_to_download); +} + +async function createZipFile(zip_file_path, file_paths) { + let output = fs.createWriteStream(zip_file_path); + + var archive = archiver('zip', { + gzip: true, + zlib: { level: 9 } // Sets the compression level. + }); + + archive.on('error', function(err) { + logger.error(err); + throw err; + }); + + // pipe archive data to the output file + archive.pipe(output); + + for (let file_path of file_paths) { + const file_name = path.parse(file_path).base; + archive.file(file_path, {name: file_name}) + } + + await archive.finalize(); + + // wait a tiny bit for the zip to reload in fs + await wait(100); + return zip_file_path; +} + function getJSONMp4(name, customPath, openReadPerms = false) { var obj = null; // output if (!customPath) customPath = config_api.getConfigItem('ytdl_video_folder_path'); @@ -193,6 +231,16 @@ function removeFileExtension(filename) { return filename_parts.join('.'); } +/** + * setTimeout, but its a promise. + * @param {number} ms + */ + async function wait(ms) { + await new Promise(resolve => { + setTimeout(resolve, ms); + }); +} + // objects function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) { @@ -221,7 +269,9 @@ module.exports = { fixVideoMetadataPerms: fixVideoMetadataPerms, deleteJSONFile: deleteJSONFile, getDownloadedFilesByType: getDownloadedFilesByType, + createContainerZipFile: createContainerZipFile, recFindByExt: recFindByExt, removeFileExtension: removeFileExtension, + wait: wait, File: File } diff --git a/src/app/components/custom-playlists/custom-playlists.component.ts b/src/app/components/custom-playlists/custom-playlists.component.ts index 8870a8e..69c7908 100644 --- a/src/app/components/custom-playlists/custom-playlists.component.ts +++ b/src/app/components/custom-playlists/custom-playlists.component.ts @@ -97,7 +97,7 @@ export class CustomPlaylistsComponent implements OnInit { const index = args.index; const dialogRef = this.dialog.open(ModifyPlaylistComponent, { data: { - playlist: playlist, + playlist_id: playlist.id, width: '65vw' } }); diff --git a/src/app/create-playlist/create-playlist.component.html b/src/app/create-playlist/create-playlist.component.html index d9f108a..ad21ec0 100644 --- a/src/app/create-playlist/create-playlist.component.html +++ b/src/app/create-playlist/create-playlist.component.html @@ -20,8 +20,8 @@ Videos {{file.id}} - {{file.id}} - {{file.id}} + {{file.id}} + {{file.id}} diff --git a/src/app/dialogs/modify-playlist/modify-playlist.component.html b/src/app/dialogs/modify-playlist/modify-playlist.component.html index 69f4cad..a8471bb 100644 --- a/src/app/dialogs/modify-playlist/modify-playlist.component.html +++ b/src/app/dialogs/modify-playlist/modify-playlist.component.html @@ -1,38 +1,40 @@

Modify playlist

- -
- - - -
- -
-
- Normal order  - Reverse order  - +
+ +
+ + +
-
- -
-
+
+
+ Normal order  + Reverse order  + +
- - - -
{{playlist_item}}
-
- - - - +
+ +
+
+ + + + +
{{playlist_item.title}}
+
+ + + + +
- + \ No newline at end of file diff --git a/src/app/dialogs/modify-playlist/modify-playlist.component.ts b/src/app/dialogs/modify-playlist/modify-playlist.component.ts index 414fc92..161cab8 100644 --- a/src/app/dialogs/modify-playlist/modify-playlist.component.ts +++ b/src/app/dialogs/modify-playlist/modify-playlist.component.ts @@ -10,8 +10,12 @@ import { PostsService } from 'app/posts.services'; }) export class ModifyPlaylistComponent implements OnInit { + playlist_id = null; + original_playlist = null; playlist = null; + playlist_file_objs = null; + available_files = []; all_files = []; playlist_updated = false; @@ -23,9 +27,8 @@ export class ModifyPlaylistComponent implements OnInit { ngOnInit(): void { if (this.data) { - this.playlist = JSON.parse(JSON.stringify(this.data.playlist)); - this.original_playlist = JSON.parse(JSON.stringify(this.data.playlist)); - this.getFiles(); + this.playlist_id = this.data.playlist_id; + this.getPlaylist(); } this.reverse_order = localStorage.getItem('default_playlist_order_reversed') === 'true'; @@ -44,11 +47,12 @@ export class ModifyPlaylistComponent implements OnInit { } processFiles(new_files = null) { - if (new_files) { this.all_files = new_files.map(file => file.id); } - this.available_files = this.all_files.filter(e => !this.playlist.fileNames.includes(e)) + if (new_files) { this.all_files = new_files; } + this.available_files = this.all_files.filter(e => !this.playlist_file_objs.includes(e)) } updatePlaylist() { + this.playlist['uids'] = this.playlist_file_objs.map(playlist_file_obj => playlist_file_obj['uid']) this.postsService.updatePlaylist(this.playlist).subscribe(res => { this.playlist_updated = true; this.postsService.openSnackBar('Playlist updated successfully.'); @@ -61,24 +65,26 @@ export class ModifyPlaylistComponent implements OnInit { } getPlaylist() { - this.postsService.getPlaylist(this.playlist.id, this.playlist.type, null).subscribe(res => { + this.postsService.getPlaylist(this.playlist_id, null, true).subscribe(res => { if (res['playlist']) { this.playlist = res['playlist']; + this.playlist_file_objs = res['file_objs']; this.original_playlist = JSON.parse(JSON.stringify(this.playlist)); + this.getFiles(); } }); } addContent(file) { - this.playlist.fileNames.push(file); + this.playlist_file_objs.push(file); this.processFiles(); } removeContent(index) { if (this.reverse_order) { - index = this.playlist.fileNames.length - 1 - index; + index = this.playlist_file_objs.length - 1 - index; } - this.playlist.fileNames.splice(index, 1); + this.playlist_file_objs.splice(index, 1); this.processFiles(); } @@ -89,10 +95,10 @@ export class ModifyPlaylistComponent implements OnInit { drop(event: CdkDragDrop) { if (this.reverse_order) { - event.previousIndex = this.playlist.fileNames.length - 1 - event.previousIndex; - event.currentIndex = this.playlist.fileNames.length - 1 - event.currentIndex; + event.previousIndex = this.playlist_file_objs.length - 1 - event.previousIndex; + event.currentIndex = this.playlist_file_objs.length - 1 - event.currentIndex; } - moveItemInArray(this.playlist.fileNames, event.previousIndex, event.currentIndex); + moveItemInArray(this.playlist_file_objs, event.previousIndex, event.currentIndex); } } diff --git a/src/app/file-card/file-card.component.ts b/src/app/file-card/file-card.component.ts index 90b906c..5596eec 100644 --- a/src/app/file-card/file-card.component.ts +++ b/src/app/file-card/file-card.component.ts @@ -84,7 +84,7 @@ export class FileCardComponent implements OnInit { editPlaylistDialog() { const dialogRef = this.dialog.open(ModifyPlaylistComponent, { data: { - playlist: this.playlist, + playlist_id: this.playlist.id, width: '65vw' } }); diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts index e37d041..cd1e3ab 100644 --- a/src/app/main/main.component.ts +++ b/src/app/main/main.component.ts @@ -657,7 +657,7 @@ export class MainComponent implements OnInit { } downloadPlaylist(playlist) { - this.postsService.downloadFileFromServer(playlist.id, null, true).subscribe(res => { + this.postsService.downloadPlaylistFromServer(playlist.id).subscribe(res => { if (playlist.id) { this.downloading_content[playlist.type][playlist.id] = false }; const blob: Blob = res; saveAs(blob, playlist.name + '.zip'); diff --git a/src/app/player/player.component.ts b/src/app/player/player.component.ts index 601271d..d0dd480 100644 --- a/src/app/player/player.component.ts +++ b/src/app/player/player.component.ts @@ -187,6 +187,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { this.postsService.getPlaylist(this.playlist_id, this.uuid, true).subscribe(res => { if (res['playlist']) { this.db_playlist = res['playlist']; + this.db_playlist['file_objs'] = res['file_objs']; this.uids = this.db_playlist.uids; this.type = res['type']; this.show_player = true; @@ -316,7 +317,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { const zipName = fileNames[0].split(' ')[0] + fileNames[1].split(' ')[0]; this.downloading = true; - this.postsService.downloadFileFromServer(this.playlist_id, this.uuid, true).subscribe(res => { + this.postsService.downloadPlaylistFromServer(this.playlist_id, this.uuid).subscribe(res => { this.downloading = false; const blob: Blob = res; saveAs(blob, zipName + '.zip'); @@ -330,7 +331,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { const ext = (this.type === 'audio') ? '.mp3' : '.mp4'; const filename = this.playlist[0].title; this.downloading = true; - this.postsService.downloadFileFromServer(this.uid, this.uuid, false).subscribe(res => { + this.postsService.downloadFileFromServer(this.uid, this.uuid, this.sub_id).subscribe(res => { this.downloading = false; const blob: Blob = res; saveAs(blob, filename + ext); diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index cf04447..f0fc20e 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -247,17 +247,29 @@ export class PostsService implements CanActivate { return this.http.post(this.path + 'downloadTwitchChatByVODID', {id: id, type: type, vodId: vodId, uuid: uuid, sub: sub}, this.httpOptions); } - downloadFileFromServer(uid, uuid = null, is_playlist = false) { + downloadFileFromServer(uid, uuid = null, sub_id = null) { return this.http.post(this.path + 'downloadFile', { uid: uid, uuid: uuid, - is_playlist: is_playlist + sub_id: sub_id }, {responseType: 'blob', params: this.httpOptions.params}); } downloadPlaylistFromServer(playlist_id, uuid = null) { - return this.http.post(this.path + 'downloadPlaylist', {playlist_id: playlist_id, uuid: uuid}); + return this.http.post(this.path + 'downloadFile', { + uuid: uuid, + playlist_id: playlist_id + }, + {responseType: 'blob', params: this.httpOptions.params}); + } + + downloadSubFromServer(sub_id, uuid = null) { + return this.http.post(this.path + 'downloadFile', { + uuid: uuid, + sub_id: sub_id + }, + {responseType: 'blob', params: this.httpOptions.params}); } checkConcurrentStream(uid) { diff --git a/src/app/subscription/subscription/subscription.component.ts b/src/app/subscription/subscription/subscription.component.ts index af08088..cb40b1c 100644 --- a/src/app/subscription/subscription/subscription.component.ts +++ b/src/app/subscription/subscription/subscription.component.ts @@ -153,15 +153,14 @@ export class SubscriptionComponent implements OnInit, OnDestroy { } this.downloading = true; - // TODO: add download subscription route - /*this.postsService.downloadFileFromServer(fileNames, 'video', this.subscription.name, true).subscribe(res => { + this.postsService.downloadSubFromServer(this.subscription.id).subscribe(res => { this.downloading = false; const blob: Blob = res; saveAs(blob, this.subscription.name + '.zip'); }, err => { console.log(err); this.downloading = false; - });*/ + }); } editSubscription() { From 4ea239170ee2e4641833ac9f5370f4aacd37de26 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 30 May 2021 00:39:00 -0600 Subject: [PATCH 064/680] If multiple videos exist in one URL, a playlist will be auto generated Removed tomp3 and tomp4 routes, replaced with /downloadFile Simplified category->playlist conversion Simplified playlist creation Simplified file deletion Playlist duration calculation is now done on the backend (categories uses this now too) removeIDFromArchive moved from subscriptions->utils Added plumbing to support type agnostic playlists --- backend/app.js | 223 +++----------- backend/authentication/auth.js | 109 +------ backend/categories.js | 21 ++ backend/db.js | 155 +++++++++- backend/subscriptions.js | 30 +- backend/utils.js | 50 ++++ .../custom-playlists.component.ts | 2 - .../recent-videos/recent-videos.component.ts | 2 +- .../create-playlist.component.ts | 35 +-- .../share-media-dialog.component.html | 3 +- .../share-media-dialog.component.ts | 6 +- src/app/main/main.component.ts | 278 ++++++------------ src/app/player/player.component.html | 8 +- src/app/player/player.component.ts | 19 +- src/app/posts.services.ts | 40 +-- 15 files changed, 381 insertions(+), 600 deletions(-) diff --git a/backend/app.js b/backend/app.js index 065b339..10db1ac 100644 --- a/backend/app.js +++ b/backend/app.js @@ -231,7 +231,7 @@ async function runFilesToDBMigration() { const file_already_in_db = db.get('files.audio').find({id: file_obj.id}).value(); if (!file_already_in_db) { logger.verbose(`Migrating file ${file_obj.id}`); - await db_api.registerFileDB(file_obj.id + '.mp3', 'audio'); + db_api.registerFileDB(file_obj.id + '.mp3', 'audio'); } } @@ -240,7 +240,7 @@ async function runFilesToDBMigration() { const file_already_in_db = db.get('files.video').find({id: file_obj.id}).value(); if (!file_already_in_db) { logger.verbose(`Migrating file ${file_obj.id}`); - await db_api.registerFileDB(file_obj.id + '.mp4', 'video'); + db_api.registerFileDB(file_obj.id + '.mp4', 'video'); } } @@ -837,87 +837,6 @@ function getVideoFormatID(name) } } -// TODO: add to db_api and support multi-user mode -async function deleteFile(uid, uuid = null, blacklistMode = false) { - const file_obj = await db_api.getVideo(uid, uuid); - const type = file_obj.isAudio ? 'audio' : 'video'; - const folderPath = path.dirname(file_obj.path); - const ext = type === 'audio' ? 'mp3' : 'mp4'; - const name = file_obj.id; - const filePathNoExtension = utils.removeFileExtension(file_obj.path); - - var jsonPath = `${file_obj.path}.info.json`; - var altJSONPath = `${filePathNoExtension}.info.json`; - var thumbnailPath = `${filePathNoExtension}.webp`; - var altThumbnailPath = `${filePathNoExtension}.jpg`; - - jsonPath = path.join(__dirname, jsonPath); - altJSONPath = path.join(__dirname, altJSONPath); - - let jsonExists = await fs.pathExists(jsonPath); - let thumbnailExists = await fs.pathExists(thumbnailPath); - - if (!jsonExists) { - if (await fs.pathExists(altJSONPath)) { - jsonExists = true; - jsonPath = altJSONPath; - } - } - - if (!thumbnailExists) { - if (await fs.pathExists(altThumbnailPath)) { - thumbnailExists = true; - thumbnailPath = altThumbnailPath; - } - } - - let fileExists = await fs.pathExists(file_obj.path); - - if (config_api.descriptors[name]) { - try { - for (let i = 0; i < config_api.descriptors[name].length; i++) { - config_api.descriptors[name][i].destroy(); - } - } catch(e) { - - } - } - - let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); - if (useYoutubeDLArchive) { - const archive_path = path.join(archivePath, `archive_${type}.txt`); - - // get ID from JSON - - var jsonobj = await (type === 'audio' ? utils.getJSONMp3(name, folderPath) : utils.getJSONMp4(name, folderPath)); - let id = null; - if (jsonobj) id = jsonobj.id; - - // use subscriptions API to remove video from the archive file, and write it to the blacklist - if (await fs.pathExists(archive_path)) { - const line = id ? await subscriptions_api.removeIDFromArchive(archive_path, id) : null; - if (blacklistMode && line) await writeToBlacklist(type, line); - } else { - logger.info('Could not find archive file for audio files. Creating...'); - await fs.close(await fs.open(archive_path, 'w')); - } - } - - if (jsonExists) await fs.unlink(jsonPath); - if (thumbnailExists) await fs.unlink(thumbnailPath); - if (fileExists) { - await fs.unlink(file_obj.path); - if (await fs.pathExists(jsonPath) || await fs.pathExists(file_obj.path)) { - return false; - } else { - return true; - } - } else { - // TODO: tell user that the file didn't exist - return true; - } -} - /** * @param {'audio' | 'video'} type * @param {string[]} fileNames @@ -1036,7 +955,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) { download['downloading'] = false; download['timestamp_end'] = Date.now(); - var file_uid = null; + var file_objs = []; let new_date = Date.now(); let difference = (new_date - date)/1000; logger.debug(`${is_audio ? 'Audio' : 'Video'} download delay: ${difference} seconds.`); @@ -1108,9 +1027,12 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) { } // registers file in DB - file_uid = db_api.registerFileDB(file_path, type, multiUserMode, null, customPath, category, options.cropFileSettings); + const file_obj = db_api.registerFileDB(file_path, type, multiUserMode, null, customPath, category, options.cropFileSettings); + // TODO: remove the following line if (file_name) file_names.push(file_name); + + file_objs.push(file_obj); } let is_playlist = file_names.length > 1; @@ -1126,12 +1048,22 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) { download['fileNames'] = is_playlist ? file_names : [full_file_path] updateDownloads(); - var videopathEncoded = encodeURIComponent(file_names[0]); + let container = null; + + if (file_objs.length > 1) { + // create playlist + const playlist_name = file_objs.map(file_obj => file_obj.title).join(', '); + const duration = file_objs.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0); + container = await db_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), type, file_objs[0]['thumbnailURL'], options.user); + } else if (file_objs.length === 1) { + container = file_objs[0]; + } else { + logger.error('Downloaded file failed to result in metadata object.'); + } resolve({ - [(type === 'audio') ? 'audiopathEncoded' : 'videopathEncoded']: videopathEncoded, - file_names: is_playlist ? file_names : null, - uid: file_uid + file_uids: file_objs.map(file_obj => file_obj.uid), + container: container }); } }); @@ -1260,7 +1192,7 @@ async function downloadFileByURL_normal(url, type, options, sessionID = null) { videopathEncoded = encodeURIComponent(utils.removeFileExtension(base_file_name)); resolve({ - [is_audio ? 'audiopathEncoded' : 'videopathEncoded']: videopathEncoded, + encodedPath: videopathEncoded, file_names: /*is_playlist ? file_names :*/ null, // playlist support is not ready uid: file_uid }); @@ -1727,18 +1659,18 @@ app.use(function(req, res, next) { app.use(compression()); -const optionalJwt = function (req, res, next) { +const optionalJwt = async function (req, res, next) { const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode'); if (multiUserMode && ((req.body && req.body.uuid) || (req.query && req.query.uuid)) && (req.path.includes('/api/getFile') || req.path.includes('/api/stream') || req.path.includes('/api/getPlaylist') || - req.path.includes('/api/downloadFile'))) { + req.path.includes('/api/downloadFileFromServer'))) { // check if shared video const using_body = req.body && req.body.uuid; const uuid = using_body ? req.body.uuid : req.query.uuid; const uid = using_body ? req.body.uid : req.query.uid; const playlist_id = using_body ? req.body.playlist_id : req.query.playlist_id; - const file = !playlist_id ? auth_api.getUserVideo(uuid, uid, true) : db_api.getPlaylist(playlist_id, uuid, true); + const file = !playlist_id ? auth_api.getUserVideo(uuid, uid, true) : await db_api.getPlaylist(playlist_id, uuid, true); if (file) { req.can_watch = true; return next(); @@ -1783,38 +1715,10 @@ app.post('/api/restartServer', optionalJwt, (req, res) => { res.send({success: true}); }); -app.post('/api/tomp3', optionalJwt, async function(req, res) { - var url = req.body.url; - var options = { - customArgs: req.body.customArgs, - customOutput: req.body.customOutput, - maxBitrate: req.body.maxBitrate, - customQualityConfiguration: req.body.customQualityConfiguration, - youtubeUsername: req.body.youtubeUsername, - youtubePassword: req.body.youtubePassword, - ui_uid: req.body.ui_uid, - user: req.isAuthenticated() ? req.user.uid : null - } - - const safeDownloadOverride = config_api.getConfigItem('ytdl_safe_download_override') || config_api.globalArgsRequiresSafeDownload(); - if (safeDownloadOverride) logger.verbose('Download is running with the safe download override.'); - const is_playlist = url.includes('playlist'); - - let result_obj = null; - if (true || safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.maxBitrate) - result_obj = await downloadFileByURL_exec(url, 'audio', options, req.query.sessionID); - else - result_obj = await downloadFileByURL_normal(url, 'audio', options, req.query.sessionID); - if (result_obj) { - res.send(result_obj); - } else { - res.sendStatus(500); - } -}); - -app.post('/api/tomp4', optionalJwt, async function(req, res) { +app.post('/api/downloadFile', optionalJwt, async function(req, res) { req.setTimeout(0); // remove timeout in case of long videos - var url = req.body.url; + const url = req.body.url; + const type = req.body.type; var options = { customArgs: req.body.customArgs, customOutput: req.body.customOutput, @@ -1833,7 +1737,7 @@ app.post('/api/tomp4', optionalJwt, async function(req, res) { let result_obj = null; if (true || safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.selectedHeight || !url.includes('youtu')) - result_obj = await downloadFileByURL_exec(url, 'video', options, req.query.sessionID); + result_obj = await downloadFileByURL_exec(url, type, options, req.query.sessionID); else result_obj = await downloadFileByURL_normal(url, 'video', options, req.query.sessionID); if (result_obj) { @@ -1936,43 +1840,22 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) { // these are returned let files = null; let playlists = null; + const uuid = req.isAuthenticated() ? req.user.uid : null; let subscriptions = config_api.getConfigItem('ytdl_allow_subscriptions') ? (subscriptions_api.getSubscriptions(req.isAuthenticated() ? req.user.uid : null)) : []; // get basic info depending on multi-user mode being enabled - if (req.isAuthenticated()) { + if (uuid) { files = auth_api.getUserVideos(req.user.uid); playlists = auth_api.getUserPlaylists(req.user.uid, files); } else { files = db.get('files').value(); playlists = JSON.parse(JSON.stringify(db.get('playlists').value())); - const categories = db.get('categories').value(); - if (categories) { - categories.forEach(category => { - const audio_files = files && files.filter(file => file.category && file.category.uid === category.uid && file.isAudio); - const video_files = files && files.filter(file => file.category && file.category.uid === category.uid && !file.isAudio); - if (audio_files && audio_files.length > 0) { - playlists.push({ - name: category['name'], - thumbnailURL: audio_files[0].thumbnailURL, - thumbnailPath: audio_files[0].thumbnailPath, - fileNames: audio_files.map(file => file.id), - type: 'audio', - auto: true - }); - } - if (video_files && video_files.length > 0) { - playlists.push({ - name: category['name'], - thumbnailURL: video_files[0].thumbnailURL, - thumbnailPath: video_files[0].thumbnailPath, - fileNames: video_files.map(file => file.id), - type: 'video', - auto: true - }); - } - }); - } + } + + const categories = categories_api.getCategoriesAsPlaylists(files); + if (categories) { + playlists = playlists.concat(categories); } // loop through subscriptions and add videos @@ -2439,26 +2322,8 @@ app.post('/api/createPlaylist', optionalJwt, async (req, res) => { let uids = req.body.uids; let type = req.body.type; let thumbnailURL = req.body.thumbnailURL; - let duration = req.body.duration; - - let new_playlist = { - name: playlistName, - uids: uids, - id: shortid.generate(), - thumbnailURL: thumbnailURL, - type: type, - registered: Date.now(), - duration: duration - }; - - if (req.isAuthenticated()) { - auth_api.addPlaylist(req.user.uid, new_playlist, type); - } else { - db.get(`playlists`) - .push(new_playlist) - .write(); - } + const new_playlist = await db_api.createPlaylist(playlistName, uids, type, thumbnailURL, req.isAuthenticated() ? req.user.uid : null); res.send({ new_playlist: new_playlist, @@ -2517,7 +2382,7 @@ app.post('/api/updatePlaylistFiles', optionalJwt, async (req, res) => { app.post('/api/updatePlaylist', optionalJwt, async (req, res) => { let playlist = req.body.playlist; - let success = db_api.updatePlaylist(playlist, req.user && req.user.uid); + let success = await db_api.updatePlaylist(playlist, req.user && req.user.uid); res.send({ success: success }); @@ -2551,20 +2416,14 @@ app.post('/api/deletePlaylist', optionalJwt, async (req, res) => { app.post('/api/deleteFile', optionalJwt, async (req, res) => { const uid = req.body.uid; const blacklistMode = req.body.blacklistMode; - - if (req.isAuthenticated()) { - let success = await auth_api.deleteUserFile(req.user.uid, uid, blacklistMode); - res.send(success); - return; - } + const uuid = req.isAuthenticated() ? req.user.uid : null; let wasDeleted = false; - wasDeleted = await deleteFile(uid, null, blacklistMode); - db.get('files').remove({uid: uid}).write(); + wasDeleted = await db_api.deleteFile(uid, uuid, blacklistMode); res.send(wasDeleted); }); -app.post('/api/downloadFile', optionalJwt, async (req, res) => { +app.post('/api/downloadFileFromServer', optionalJwt, async (req, res) => { let uid = req.body.uid; let uuid = req.body.uuid; let playlist_id = req.body.playlist_id; diff --git a/backend/authentication/auth.js b/backend/authentication/auth.js index 3af31e7..6e83ec3 100644 --- a/backend/authentication/auth.js +++ b/backend/authentication/auth.js @@ -1,12 +1,10 @@ const path = require('path'); const config_api = require('../config'); const consts = require('../consts'); -var subscriptions_api = require('../subscriptions') const fs = require('fs-extra'); -var jwt = require('jsonwebtoken'); +const jwt = require('jsonwebtoken'); const { uuid } = require('uuidv4'); -var bcrypt = require('bcryptjs'); - +const bcrypt = require('bcryptjs'); var LocalStrategy = require('passport-local').Strategy; var LdapStrategy = require('passport-ldapauth'); @@ -299,11 +297,6 @@ exports.getUserVideo = function(user_uid, file_uid, requireSharing = false) { return file; } -exports.addPlaylist = function(user_uid, new_playlist) { - users_db.get('users').find({uid: user_uid}).get(`playlists`).push(new_playlist).write(); - return true; -} - exports.updatePlaylistFiles = function(user_uid, playlistID, new_filenames) { users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlistID}).assign({fileNames: new_filenames}); return true; @@ -317,35 +310,6 @@ exports.removePlaylist = function(user_uid, playlistID) { exports.getUserPlaylists = function(user_uid, user_files = null) { const user = users_db.get('users').find({uid: user_uid}).value(); const playlists = JSON.parse(JSON.stringify(user['playlists'])); - const categories = db.get('categories').value(); - if (categories && user_files) { - categories.forEach(category => { - const audio_files = user_files && user_files.filter(file => file.category && file.category.uid === category.uid && file.isAudio); - const video_files = user_files && user_files.filter(file => file.category && file.category.uid === category.uid && !file.isAudio); - if (audio_files && audio_files.length > 0) { - playlists.push({ - name: category['name'], - thumbnailURL: audio_files[0].thumbnailURL, - thumbnailPath: audio_files[0].thumbnailPath, - fileNames: audio_files.map(file => file.id), - type: 'audio', - uid: user_uid, - auto: true - }); - } - if (video_files && video_files.length > 0) { - playlists.push({ - name: category['name'], - thumbnailURL: video_files[0].thumbnailURL, - thumbnailPath: video_files[0].thumbnailPath, - fileNames: video_files.map(file => file.id), - type: 'video', - uid: user_uid, - auto: true - }); - } - }); - } return playlists; } @@ -369,75 +333,6 @@ exports.registerUserFile = function(user_uid, file_object) { .write(); } -exports.deleteUserFile = async function(user_uid, file_uid, blacklistMode = false) { - let success = false; - const file_obj = users_db.get('users').find({uid: user_uid}).get(`files`).find({uid: file_uid}).value(); - if (file_obj) { - const type = file_obj.isAudio ? 'audio' : 'video'; - const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); - const ext = type === 'audio' ? '.mp3' : '.mp4'; - - // close descriptors - if (config_api.descriptors[file_obj.id]) { - try { - for (let i = 0; i < config_api.descriptors[file_obj.id].length; i++) { - config_api.descriptors[file_obj.id][i].destroy(); - } - } catch(e) { - - } - } - - const full_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext); - users_db.get('users').find({uid: user_uid}).get(`files`) - .remove({ - uid: file_uid - }).write(); - if (await fs.pathExists(full_path)) { - // remove json and file - const json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + '.info.json'); - const alternate_json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext + '.info.json'); - let youtube_id = null; - if (await fs.pathExists(json_path)) { - youtube_id = await fs.readJSON(json_path).id; - await fs.unlink(json_path); - } else if (await fs.pathExists(alternate_json_path)) { - youtube_id = await fs.readJSON(alternate_json_path).id; - await fs.unlink(alternate_json_path); - } - - await fs.unlink(full_path); - - // do archive stuff - - let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); - if (useYoutubeDLArchive) { - const archive_path = path.join(usersFileFolder, user_uid, 'archives', `archive_${type}.txt`); - - // use subscriptions API to remove video from the archive file, and write it to the blacklist - if (await fs.pathExists(archive_path)) { - const line = youtube_id ? await subscriptions_api.removeIDFromArchive(archive_path, youtube_id) : null; - if (blacklistMode && line) { - let blacklistPath = path.join(usersFileFolder, user_uid, 'archives', `blacklist_${type}.txt`); - // adds newline to the beginning of the line - line = '\n' + line; - await fs.appendFile(blacklistPath, line); - } - } else { - logger.info(`Could not find archive file for ${type} files. Creating...`); - await fs.ensureFile(archive_path); - } - } - } - success = true; - } else { - success = false; - logger.warn(`User file ${file_uid} does not exist!`); - } - - return success; -} - exports.changeSharingMode = function(user_uid, file_uid, is_playlist, enabled) { let success = false; const user_db_obj = users_db.get('users').find({uid: user_uid}); diff --git a/backend/categories.js b/backend/categories.js index 2134373..ce56d5c 100644 --- a/backend/categories.js +++ b/backend/categories.js @@ -1,4 +1,5 @@ const config_api = require('./config'); +const utils = require('./utils'); var logger = null; var db = null; @@ -68,6 +69,24 @@ function getCategories() { return categories ? categories : null; } +function getCategoriesAsPlaylists(files = null) { + const categories_as_playlists = []; + const available_categories = getCategories(); + if (available_categories && files) { + for (category of available_categories) { + const files_that_match = utils.addUIDsToCategory(category, files); + if (files_that_match && files_that_match.length > 0) { + category['thumbnailURL'] = files_that_match[0].thumbnailURL; + category['thumbnailPath'] = files_that_match[0].thumbnailPath; + category['duration'] = files_that_match.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0); + category['id'] = category['uid']; + categories_as_playlists.push(category); + } + } + } + return categories_as_playlists; +} + function applyCategoryRules(file_json, rules, category_name) { let rules_apply = false; for (let i = 0; i < rules.length; i++) { @@ -126,4 +145,6 @@ async function addTagToExistingTags(tag) { module.exports = { initialize: initialize, categorize: categorize, + getCategories: getCategories, + getCategoriesAsPlaylists: getCategoriesAsPlaylists } \ No newline at end of file diff --git a/backend/db.js b/backend/db.js index 106c3f2..719161c 100644 --- a/backend/db.js +++ b/backend/db.js @@ -53,14 +53,14 @@ exports.registerFileDB = (file_path, type, multiUserMode = null, sub = null, cus } } - const file_uid = registerFileDBManual(db_path, file_object); + const file_obj = registerFileDBManual(db_path, file_object); // remove metadata JSON if needed if (!config_api.getConfigItem('ytdl_include_metadata')) { utils.deleteJSONFile(file_id, type, multiUserMode && multiUserMode.file_path) } - return file_uid; + return file_obj; } function registerFileDBManual(db_path, file_object) { @@ -75,7 +75,7 @@ function registerFileDBManual(db_path, file_object) { // add new file to db db_path.push(file_object).write(); - return file_object['uid']; + return file_object; } function generateFileObject(id, type, customPath = null, sub = null) { @@ -224,17 +224,47 @@ exports.preimportUnregisteredSubscriptionFile = async (sub, appendedBasePath) => return preimported_file_paths; } +exports.createPlaylist = async (playlist_name, uids, type, thumbnail_url, user_uid = null) => { + let new_playlist = { + name: playlist_name, + uids: uids, + id: uuid(), + thumbnailURL: thumbnail_url, + type: type, + registered: Date.now(), + }; + + const duration = await exports.calculatePlaylistDuration(new_playlist, user_uid); + new_playlist.duration = duration; + + if (user_uid) { + users_db.get('users').find({uid: user_uid}).get(`playlists`).push(new_playlist).write(); + } else { + db.get(`playlists`) + .push(new_playlist) + .write(); + } + + return new_playlist; +} + exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = false) => { let playlist = null if (user_uid) { playlist = users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlist_id}).value(); - - // prevent unauthorized users from accessing the file info - if (require_sharing && !playlist['sharingEnabled']) return null; } else { playlist = db.get(`playlists`).find({id: playlist_id}).value(); } + if (!playlist) { + playlist = db.get('categories').find({uid: playlist_id}).value(); + if (playlist) { + // category found + const files = await exports.getFiles(user_uid); + utils.addUIDsToCategory(playlist, files); + } + } + // converts playlists to new UID-based schema if (playlist && playlist['fileNames'] && !playlist['uids']) { playlist['uids'] = []; @@ -248,11 +278,18 @@ exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = fal exports.updatePlaylist(playlist, user_uid); } + // prevent unauthorized users from accessing the file info + if (require_sharing && !playlist['sharingEnabled']) return null; + return playlist; } -exports.updatePlaylist = (playlist, user_uid = null) => { +exports.updatePlaylist = async (playlist, user_uid = null) => { let playlistID = playlist.id; + + const duration = await exports.calculatePlaylistDuration(playlist, user_uid); + playlist.duration = duration; + let db_loc = null; if (user_uid) { db_loc = users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlistID}); @@ -263,6 +300,103 @@ exports.updatePlaylist = (playlist, user_uid = null) => { return true; } +exports.calculatePlaylistDuration = async (playlist, uuid, playlist_file_objs = null) => { + if (!playlist_file_objs) { + playlist_file_objs = []; + for (let i = 0; i < playlist['uids'].length; i++) { + const uid = playlist['uids'][i]; + const file_obj = await exports.getVideo(uid, uuid); + if (file_obj) playlist_file_objs.push(file_obj); + } + } + + return playlist_file_objs.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0); +} + +exports.deleteFile = async (uid, uuid = null, blacklistMode = false) => { + const file_obj = await exports.getVideo(uid, uuid); + const type = file_obj.isAudio ? 'audio' : 'video'; + const folderPath = path.dirname(file_obj.path); + const ext = type === 'audio' ? 'mp3' : 'mp4'; + const name = file_obj.id; + const filePathNoExtension = utils.removeFileExtension(file_obj.path); + + var jsonPath = `${file_obj.path}.info.json`; + var altJSONPath = `${filePathNoExtension}.info.json`; + var thumbnailPath = `${filePathNoExtension}.webp`; + var altThumbnailPath = `${filePathNoExtension}.jpg`; + + jsonPath = path.join(__dirname, jsonPath); + altJSONPath = path.join(__dirname, altJSONPath); + + let jsonExists = await fs.pathExists(jsonPath); + let thumbnailExists = await fs.pathExists(thumbnailPath); + + if (!jsonExists) { + if (await fs.pathExists(altJSONPath)) { + jsonExists = true; + jsonPath = altJSONPath; + } + } + + if (!thumbnailExists) { + if (await fs.pathExists(altThumbnailPath)) { + thumbnailExists = true; + thumbnailPath = altThumbnailPath; + } + } + + let fileExists = await fs.pathExists(file_obj.path); + + if (config_api.descriptors[uid]) { + try { + for (let i = 0; i < config_api.descriptors[uid].length; i++) { + config_api.descriptors[uid][i].destroy(); + } + } catch(e) { + + } + } + + let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); + if (useYoutubeDLArchive) { + const archive_path = uuid ? path.join(usersFileFolder, uuid, 'archives', `archive_${type}.txt`) : path.join('appdata', 'archives', `archive_${type}.txt`); + + // get ID from JSON + + var jsonobj = await (type === 'audio' ? utils.getJSONMp3(name, folderPath) : utils.getJSONMp4(name, folderPath)); + let id = null; + if (jsonobj) id = jsonobj.id; + + // use subscriptions API to remove video from the archive file, and write it to the blacklist + if (await fs.pathExists(archive_path)) { + const line = id ? await utils.removeIDFromArchive(archive_path, id) : null; + if (blacklistMode && line) await writeToBlacklist(type, line); + } else { + logger.info('Could not find archive file for audio files. Creating...'); + await fs.close(await fs.open(archive_path, 'w')); + } + } + + if (jsonExists) await fs.unlink(jsonPath); + if (thumbnailExists) await fs.unlink(thumbnailPath); + + const base_db_path = uuid ? users_db.get('users').find({uid: uuid}) : db; + base_db_path.get('files').remove({uid: uid}).write(); + + if (fileExists) { + await fs.unlink(file_obj.path); + if (await fs.pathExists(jsonPath) || await fs.pathExists(file_obj.path)) { + return false; + } else { + return true; + } + } else { + // TODO: tell user that the file didn't exist + return true; + } +} + // Video ID is basically just the file name without the base path and file extension - this method helps us get away from that exports.getVideoUIDByID = (file_id, uuid = null) => { const base_db_path = uuid ? users_db.get('users').find({uid: uuid}) : db; @@ -270,12 +404,17 @@ exports.getVideoUIDByID = (file_id, uuid = null) => { return file_obj ? file_obj['uid'] : null; } -exports.getVideo = async (file_uid, uuid, sub_id) => { +exports.getVideo = async (file_uid, uuid = null, sub_id = null) => { const base_db_path = uuid ? users_db.get('users').find({uid: uuid}) : db; const sub_db_path = sub_id ? base_db_path.get('subscriptions').find({id: sub_id}).get('videos') : base_db_path.get('files'); return sub_db_path.find({uid: file_uid}).value(); } +exports.getFiles = async (uuid = null) => { + const base_db_path = uuid ? users_db.get('users').find({uid: uuid}) : db; + return base_db_path.get('files').value(); +} + exports.setVideoProperty = async (file_uid, assignment_obj, uuid, sub_id) => { const base_db_path = uuid ? users_db.get('users').find({uid: uuid}) : db; const sub_db_path = sub_id ? base_db_path.get('subscriptions').find({id: sub_id}).get('videos') : base_db_path.get('files'); diff --git a/backend/subscriptions.js b/backend/subscriptions.js index 7ff1d06..8f29cae 100644 --- a/backend/subscriptions.js +++ b/backend/subscriptions.js @@ -243,7 +243,7 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, const archive_path = path.join(sub.archive, 'archive.txt') // if archive exists, remove line with video ID if (await fs.pathExists(archive_path)) { - await removeIDFromArchive(archive_path, retrievedID); + utils.removeIDFromArchive(archive_path, retrievedID); } } return true; @@ -597,33 +597,6 @@ function getAppendedBasePath(sub, base_path) { return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name); } -async function removeIDFromArchive(archive_path, id) { - let data = await fs.readFile(archive_path, {encoding: 'utf-8'}); - if (!data) { - logger.error('Archive could not be found.'); - return; - } - - let dataArray = data.split('\n'); // convert file data in an array - const searchKeyword = id; // we are looking for a line, contains, key word id in the file - let lastIndex = -1; // let say, we have not found the keyword - - for (let index=0; index= 0; i--) { + num_sum += parseInt(dur_str_parts[i])*(60**(dur_str_parts.length-1-i)); + } + return num_sum; +} + +function getMatchingCategoryFiles(category, files) { + return files && files.filter(file => file.category && file.category.uid === category.uid); +} + +function addUIDsToCategory(category, files) { + const files_that_match = getMatchingCategoryFiles(category, files); + category['uids'] = files_that_match.map(file => file.uid); + return files_that_match; +} async function recFindByExt(base,ext,files,result) { @@ -268,8 +314,12 @@ module.exports = { getExpectedFileSize: getExpectedFileSize, fixVideoMetadataPerms: fixVideoMetadataPerms, deleteJSONFile: deleteJSONFile, + removeIDFromArchive, removeIDFromArchive, getDownloadedFilesByType: getDownloadedFilesByType, createContainerZipFile: createContainerZipFile, + durationStringToNumber: durationStringToNumber, + getMatchingCategoryFiles: getMatchingCategoryFiles, + addUIDsToCategory: addUIDsToCategory, recFindByExt: recFindByExt, removeFileExtension: removeFileExtension, wait: wait, diff --git a/src/app/components/custom-playlists/custom-playlists.component.ts b/src/app/components/custom-playlists/custom-playlists.component.ts index 69c7908..3914586 100644 --- a/src/app/components/custom-playlists/custom-playlists.component.ts +++ b/src/app/components/custom-playlists/custom-playlists.component.ts @@ -53,14 +53,12 @@ export class CustomPlaylistsComponent implements OnInit { goToPlaylist(info_obj) { const playlist = info_obj.file; const playlistID = playlist.id; - const type = playlist.type; if (playlist) { if (this.postsService.config['Extra']['download_only_mode']) { this.downloadPlaylist(playlist.id, playlist.name); } else { localStorage.setItem('player_navigator', this.router.url); - const fileNames = playlist.fileNames; this.router.navigate(['/player', {playlist_id: playlistID, auto: playlist.auto}]); } } else { diff --git a/src/app/components/recent-videos/recent-videos.component.ts b/src/app/components/recent-videos/recent-videos.component.ts index 31ed771..05deb2b 100644 --- a/src/app/components/recent-videos/recent-videos.component.ts +++ b/src/app/components/recent-videos/recent-videos.component.ts @@ -221,7 +221,7 @@ export class RecentVideosComponent implements OnInit { if (!this.postsService.config.Extra.file_manager_enabled) { // tell server to delete the file once downloaded - this.postsService.deleteFile(name, type).subscribe(delRes => { + this.postsService.deleteFile(file.uid).subscribe(delRes => { // reload mp4s this.getAllFiles(); }); diff --git a/src/app/create-playlist/create-playlist.component.ts b/src/app/create-playlist/create-playlist.component.ts index b9cf976..c22d32d 100644 --- a/src/app/create-playlist/create-playlist.component.ts +++ b/src/app/create-playlist/create-playlist.component.ts @@ -51,9 +51,8 @@ export class CreatePlaylistComponent implements OnInit { createPlaylist() { const thumbnailURL = this.getThumbnailURL(); - const duration = this.calculateDuration(); this.create_in_progress = true; - this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL, duration).subscribe(res => { + this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL).subscribe(res => { this.create_in_progress = false; if (res['success']) { this.dialogRef.close(true); @@ -78,36 +77,4 @@ export class CreatePlaylistComponent implements OnInit { } return null; } - - getDuration(file_id) { - let properFilesToSelectFrom = this.filesToSelectFrom; - if (!this.filesToSelectFrom) { - properFilesToSelectFrom = this.type === 'audio' ? this.audiosToSelectFrom : this.videosToSelectFrom; - } - for (let i = 0; i < properFilesToSelectFrom.length; i++) { - const file = properFilesToSelectFrom[i]; - if (file.id === file_id) { - return file.duration; - } - } - return null; - } - - calculateDuration() { - let sum = 0; - for (let i = 0; i < this.filesSelect.value.length; i++) { - const duration_val = this.getDuration(this.filesSelect.value[i]); - sum += typeof duration_val === 'string' ? this.durationStringToNumber(duration_val) : duration_val; - } - return sum; - } - - durationStringToNumber(dur_str) { - let num_sum = 0; - const dur_str_parts = dur_str.split(':'); - for (let i = dur_str_parts.length-1; i >= 0; i--) { - num_sum += parseInt(dur_str_parts[i])*(60**(dur_str_parts.length-1-i)); - } - return num_sum; - } } diff --git a/src/app/dialogs/share-media-dialog/share-media-dialog.component.html b/src/app/dialogs/share-media-dialog/share-media-dialog.component.html index fcd8f3c..7175b52 100644 --- a/src/app/dialogs/share-media-dialog/share-media-dialog.component.html +++ b/src/app/dialogs/share-media-dialog/share-media-dialog.component.html @@ -1,7 +1,6 @@

Share playlist - Share video - Share audio + Share file

diff --git a/src/app/dialogs/share-media-dialog/share-media-dialog.component.ts b/src/app/dialogs/share-media-dialog/share-media-dialog.component.ts index 9b687ff..332a461 100644 --- a/src/app/dialogs/share-media-dialog/share-media-dialog.component.ts +++ b/src/app/dialogs/share-media-dialog/share-media-dialog.component.ts @@ -11,7 +11,6 @@ import { PostsService } from 'app/posts.services'; }) export class ShareMediaDialogComponent implements OnInit { - type = null; uid = null; uuid = null; share_url = null; @@ -26,7 +25,6 @@ export class ShareMediaDialogComponent implements OnInit { ngOnInit(): void { if (this.data) { - this.type = this.data.type; this.uid = this.data.uid; this.uuid = this.data.uuid; this.sharing_enabled = this.data.sharing_enabled; @@ -65,7 +63,7 @@ export class ShareMediaDialogComponent implements OnInit { sharingChanged(event) { if (event.checked) { - this.postsService.enableSharing(this.uid, this.type, this.is_playlist).subscribe(res => { + this.postsService.enableSharing(this.uid, this.is_playlist).subscribe(res => { if (res['success']) { this.openSnackBar('Sharing enabled.'); this.sharing_enabled = true; @@ -76,7 +74,7 @@ export class ShareMediaDialogComponent implements OnInit { this.openSnackBar('Failed to enable sharing - server error.'); }); } else { - this.postsService.disableSharing(this.uid, this.type, this.is_playlist).subscribe(res => { + this.postsService.disableSharing(this.uid, this.is_playlist).subscribe(res => { if (res['success']) { this.openSnackBar('Sharing disabled.'); this.sharing_enabled = false; diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts index cd1e3ab..bf99b73 100644 --- a/src/app/main/main.component.ts +++ b/src/app/main/main.component.ts @@ -342,12 +342,8 @@ export class MainComponent implements OnInit { } } - public goToFile(name, isAudio, uid) { - if (isAudio) { - this.downloadHelperMp3(name, uid, false, false, null, true); - } else { - this.downloadHelperMp4(name, uid, false, false, null, true); - } + public goToFile(container, isAudio, uid) { + this.downloadHelper(container, isAudio ? 'audio' : 'video', false, false, null, true); } public goToPlaylist(playlistID, type) { @@ -379,56 +375,26 @@ export class MainComponent implements OnInit { // download helpers - downloadHelperMp3(name, uid, is_playlist = false, forceView = false, new_download = null, navigate_mode = false) { + downloadHelper(container, type, is_playlist = false, force_view = false, new_download = null, navigate_mode = false) { this.downloadingfile = false; if (this.multiDownloadMode && !this.downloadOnlyMode && !navigate_mode) { // do nothing this.reloadRecentVideos(); } else { // if download only mode, just download the file. no redirect - if (forceView === false && this.downloadOnlyMode && !this.iOS) { + if (force_view === false && this.downloadOnlyMode && !this.iOS) { if (is_playlist) { - const zipName = name[0].split(' ')[0] + name[1].split(' ')[0]; - this.downloadPlaylist(name, 'audio', zipName); + this.downloadPlaylist(container['uid']); } else { - this.downloadAudioFile(decodeURI(name)); + this.downloadFileFromServer(container, type); } this.reloadRecentVideos(); } else { localStorage.setItem('player_navigator', this.router.url.split(';')[0]); if (is_playlist) { - this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'audio'}]); + this.router.navigate(['/player', {playlist_id: container['id'], type: type}]); } else { - this.router.navigate(['/player', {type: 'audio', uid: uid}]); - } - } - } - - // remove download from current downloads - this.removeDownloadFromCurrentDownloads(new_download); - } - - downloadHelperMp4(name, uid, is_playlist = false, forceView = false, new_download = null, navigate_mode = false) { - this.downloadingfile = false; - if (this.multiDownloadMode && !this.downloadOnlyMode && !navigate_mode) { - // do nothing - this.reloadRecentVideos(); - } else { - // if download only mode, just download the file. no redirect - if (forceView === false && this.downloadOnlyMode) { - if (is_playlist) { - const zipName = name[0].split(' ')[0] + name[1].split(' ')[0]; - this.downloadPlaylist(name, 'video', zipName); - } else { - this.downloadVideoFile(decodeURI(name)); - } - this.reloadRecentVideos(); - } else { - localStorage.setItem('player_navigator', this.router.url.split(';')[0]); - if (is_playlist) { - this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'video'}]); - } else { - this.router.navigate(['/player', {type: 'video', uid: uid}]); + this.router.navigate(['/player', {type: type, uid: container['uid']}]); } } } @@ -439,133 +405,85 @@ export class MainComponent implements OnInit { // download click handler downloadClicked() { - if (this.ValidURL(this.url)) { - this.urlError = false; - this.path = ''; - - // get common args - const customArgs = (this.customArgsEnabled ? this.customArgs : null); - const customOutput = (this.customOutputEnabled ? this.customOutput : null); - const youtubeUsername = (this.youtubeAuthEnabled && this.youtubeUsername ? this.youtubeUsername : null); - const youtubePassword = (this.youtubeAuthEnabled && this.youtubePassword ? this.youtubePassword : null); - - // set advanced inputs - if (this.allowAdvancedDownload) { - if (customArgs) { - localStorage.setItem('customArgs', customArgs); - } - if (customOutput) { - localStorage.setItem('customOutput', customOutput); - } - if (youtubeUsername) { - localStorage.setItem('youtubeUsername', youtubeUsername); - } - } - - if (this.audioOnly) { - // create download object - const new_download: Download = { - uid: uuid(), - type: 'audio', - percent_complete: 0, - url: this.url, - downloading: true, - is_playlist: this.url.includes('playlist'), - error: false - }; - this.downloads.push(new_download); - if (!this.current_download && !this.multiDownloadMode) { this.current_download = new_download }; - this.downloadingfile = true; - - let customQualityConfiguration = null; - if (this.selectedQuality !== '') { - customQualityConfiguration = this.getSelectedAudioFormat(); - } - - this.postsService.makeMP3(this.url, (this.selectedQuality === '' ? null : this.selectedQuality), - customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword, new_download.uid).subscribe(posts => { - // update download object - new_download.downloading = false; - new_download.percent_complete = 100; - - const is_playlist = !!(posts['file_names']); - this.path = is_playlist ? posts['file_names'] : posts['audiopathEncoded']; - - this.current_download = null; - - if (this.path !== '-1') { - this.downloadHelperMp3(this.path, posts['uid'], is_playlist, false, new_download); - } - }, error => { // can't access server or failed to download for other reasons - this.downloadingfile = false; - this.current_download = null; - new_download['downloading'] = false; - // removes download from list of downloads - const downloads_index = this.downloads.indexOf(new_download); - if (downloads_index !== -1) { - this.downloads.splice(downloads_index) - } - this.openSnackBar('Download failed!', 'OK.'); - }); - } else { - // create download object - const new_download: Download = { - uid: uuid(), - type: 'video', - percent_complete: 0, - url: this.url, - downloading: true, - is_playlist: this.url.includes('playlist'), - error: false - }; - this.downloads.push(new_download); - if (!this.current_download && !this.multiDownloadMode) { this.current_download = new_download }; - this.downloadingfile = true; - - const customQualityConfiguration = this.getSelectedVideoFormat(); - - let cropFileSettings = null; - - if (this.cropFile) { - cropFileSettings = { - cropFileStart: this.cropFileStart, - cropFileEnd: this.cropFileEnd - } - } - - this.postsService.makeMP4(this.url, (this.selectedQuality === '' ? null : this.selectedQuality), - customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword, new_download.uid, cropFileSettings).subscribe(posts => { - // update download object - new_download.downloading = false; - new_download.percent_complete = 100; - - const is_playlist = !!(posts['file_names']); - this.path = is_playlist ? posts['file_names'] : posts['videopathEncoded']; - - this.current_download = null; - - if (this.path !== '-1') { - this.downloadHelperMp4(this.path, posts['uid'], is_playlist, false, new_download); - } - }, error => { // can't access server - this.downloadingfile = false; - this.current_download = null; - new_download['downloading'] = false; - // removes download from list of downloads - const downloads_index = this.downloads.indexOf(new_download); - if (downloads_index !== -1) { - this.downloads.splice(downloads_index) - } - this.openSnackBar('Download failed!', 'OK.'); - }); - } - - if (this.multiDownloadMode) { - this.url = ''; - this.downloadingfile = false; - } - } else { + if (!this.ValidURL(this.url)) { this.urlError = true; + return; + } + + this.urlError = false; + + // get common args + const customArgs = (this.customArgsEnabled ? this.customArgs : null); + const customOutput = (this.customOutputEnabled ? this.customOutput : null); + const youtubeUsername = (this.youtubeAuthEnabled && this.youtubeUsername ? this.youtubeUsername : null); + const youtubePassword = (this.youtubeAuthEnabled && this.youtubePassword ? this.youtubePassword : null); + + // set advanced inputs + if (this.allowAdvancedDownload) { + if (customArgs) { + localStorage.setItem('customArgs', customArgs); + } + if (customOutput) { + localStorage.setItem('customOutput', customOutput); + } + if (youtubeUsername) { + localStorage.setItem('youtubeUsername', youtubeUsername); + } + } + + const type = this.audioOnly ? 'audio' : 'video'; + // create download object + const new_download: Download = { + uid: uuid(), + type: type, + percent_complete: 0, + url: this.url, + downloading: true, + is_playlist: this.url.includes('playlist'), + error: false + }; + this.downloads.push(new_download); + if (!this.current_download && !this.multiDownloadMode) { this.current_download = new_download }; + this.downloadingfile = true; + + let customQualityConfiguration = type === 'audio' ? this.getSelectedAudioFormat() : this.getSelectedVideoFormat(); + + let cropFileSettings = null; + + if (this.cropFile) { + cropFileSettings = { + cropFileStart: this.cropFileStart, + cropFileEnd: this.cropFileEnd + } + } + + this.postsService.downloadFile(this.url, type, (this.selectedQuality === '' ? null : this.selectedQuality), + customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword, new_download.uid, cropFileSettings).subscribe(res => { + // update download object + new_download.downloading = false; + new_download.percent_complete = 100; + + const container = res['container']; + const is_playlist = res['file_uids'].length > 1; + + this.current_download = null; + + this.downloadHelper(container, type, is_playlist, false, new_download); + }, error => { // can't access server + this.downloadingfile = false; + this.current_download = null; + new_download['downloading'] = false; + // removes download from list of downloads + const downloads_index = this.downloads.indexOf(new_download); + if (downloads_index !== -1) { + this.downloads.splice(downloads_index) + } + this.openSnackBar('Download failed!', 'OK.'); + }); + + if (this.multiDownloadMode) { + this.url = ''; + this.downloadingfile = false; } } @@ -626,27 +544,13 @@ export class MainComponent implements OnInit { } } - downloadAudioFile(file) { - this.downloading_content['audio'][file.id] = true; + downloadFileFromServer(file, type) { + const ext = type === 'audio' ? 'mp3' : 'mp4' + this.downloading_content[type][file.id] = true; this.postsService.downloadFileFromServer(file.uid).subscribe(res => { - this.downloading_content['audio'][file.id] = false; + this.downloading_content[type][file.id] = false; const blob: Blob = res; - saveAs(blob, decodeURIComponent(file.id) + '.mp3'); - - if (!this.fileManagerEnabled) { - // tell server to delete the file once downloaded - this.postsService.deleteFile(file.uid).subscribe(delRes => { - }); - } - }); - } - - downloadVideoFile(file) { - this.downloading_content['video'][file.id] = true; - this.postsService.downloadFileFromServer(file.uid).subscribe(res => { - this.downloading_content['video'][file.id] = false; - const blob: Blob = res; - saveAs(blob, decodeURIComponent(file.id) + '.mp4'); + saveAs(blob, decodeURIComponent(file.id) + `.${ext}`); if (!this.fileManagerEnabled) { // tell server to delete the file once downloaded diff --git a/src/app/player/player.component.html b/src/app/player/player.component.html index 9de791a..fc8dbac 100644 --- a/src/app/player/player.component.html +++ b/src/app/player/player.component.html @@ -1,10 +1,10 @@
-
+
-
- -