From 1d5490c0ff9195bfcb8acf778cee6e17e3509e38 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Tue, 12 Jan 2021 22:08:42 -0500 Subject: [PATCH 01/47] 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 02/47] 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 6481102e01ab96e1326e70a0e9adb77632e74123 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 13 Jan 2021 16:12:11 -0500 Subject: [PATCH 03/47] 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 00a0ab460bbf59955c56651d52788cb9e0a1f433 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Wed, 20 Jan 2021 08:37:14 -0500 Subject: [PATCH 04/47] 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 05/47] 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 06/47] 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 07/47] 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 08/47] 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 09/47] 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 9d09eeffe313a0c872fc78d89bffc57e1800c6bb Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Mon, 22 Feb 2021 12:54:28 -0700 Subject: [PATCH 10/47] 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 11/47] 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 1f0153b17edae9000cb0b0f58d12d082853b8291 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Tue, 16 Mar 2021 20:06:05 -0600 Subject: [PATCH 12/47] 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 4643efbae04a6c00a1a29be2d96cb7e0dc12c836 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Tue, 16 Mar 2021 22:41:07 -0600 Subject: [PATCH 13/47] 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 14/47] 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 15/47] 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 16/47] 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 17/47] 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 18/47] 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 19/47] 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 20/47] 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 21/47] 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 22/47] 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 23/47] 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 24/47] 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 25/47] 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 26/47] 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 27/47] 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 28/47] 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 29/47] 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 30/47] 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 31/47] 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 32/47] 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 33/47] 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 @@
-
+
-
- -