mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-08 04:20:08 +03:00
Compare commits
98 Commits
improved-d
...
angular-up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e3a552392 | ||
|
|
24475386f9 | ||
|
|
55268301f6 | ||
|
|
faa76abbbd | ||
|
|
b827f8f0cc | ||
|
|
b6b61c42d4 | ||
|
|
6af1ce4092 | ||
|
|
303d0015c6 | ||
|
|
56db43da79 | ||
|
|
64b1a9e5c0 | ||
|
|
48f0a700ab | ||
|
|
768798c6b3 | ||
|
|
9d1f93acfb | ||
|
|
077a0d8fdb | ||
|
|
c9359f172e | ||
|
|
d6dc4756a7 | ||
|
|
9bc9b17294 | ||
|
|
80d3580447 | ||
|
|
3f15f3bcaf | ||
|
|
703848e4e5 | ||
|
|
934965720e | ||
|
|
bb4a882d19 | ||
|
|
74315b8c76 | ||
|
|
a9e95c5bb8 | ||
|
|
fe45a889c9 | ||
|
|
e726e991cc | ||
|
|
940267651d | ||
|
|
2dc68139f7 | ||
|
|
301451d021 | ||
|
|
a7f8795e7e | ||
|
|
162094a9b9 | ||
|
|
e843b4c97f | ||
|
|
c784091ad6 | ||
|
|
fb404d3cee | ||
|
|
68c2ee26ff | ||
|
|
742129bf6a | ||
|
|
9a250b5c58 | ||
|
|
86cbfea08f | ||
|
|
19a3ffc118 | ||
|
|
d225e84a03 | ||
|
|
a4a0045475 | ||
|
|
573cca0b2f | ||
|
|
fecefde3ad | ||
|
|
d300a8a3c6 | ||
|
|
c6b7e7bc4c | ||
|
|
0ffd7022d0 | ||
|
|
a0c36bf1a1 | ||
|
|
64b4b5a2b4 | ||
|
|
b1448d95e5 | ||
|
|
a2d1b154a3 | ||
|
|
2ba1dc6333 | ||
|
|
314a5047d6 | ||
|
|
fb6cf8548d | ||
|
|
adbe7f95d5 | ||
|
|
55d4f746c3 | ||
|
|
6d3f5e6c94 | ||
|
|
bec158f65d | ||
|
|
6fa4296edf | ||
|
|
636f7b16a8 | ||
|
|
39abc3efcf | ||
|
|
2edc00c950 | ||
|
|
6fe0cd5649 | ||
|
|
b6de6d08fa | ||
|
|
cddd280206 | ||
|
|
9d1624d569 | ||
|
|
da8c23d3ef | ||
|
|
901e87a681 | ||
|
|
7bfb2976fe | ||
|
|
295781b1f1 | ||
|
|
cbdd1a6253 | ||
|
|
c91e51de15 | ||
|
|
4c6c15d3a3 | ||
|
|
0951e445ac | ||
|
|
e1cb56e8e9 | ||
|
|
05ee48ffb6 | ||
|
|
7f47fb339b | ||
|
|
690cc38899 | ||
|
|
d912e44484 | ||
|
|
93e3dafb03 | ||
|
|
b5ee0d365c | ||
|
|
0dd617b438 | ||
|
|
aca86e0228 | ||
|
|
895c385d6b | ||
|
|
b861e54a51 | ||
|
|
8d8c52e009 | ||
|
|
37107148eb | ||
|
|
29273e2775 | ||
|
|
24659213c2 | ||
|
|
2354749c2f | ||
|
|
5cd3669634 | ||
|
|
2328502c0d | ||
|
|
301d8a6ae3 | ||
|
|
8529fe152c | ||
|
|
feff8b2461 | ||
|
|
9ab15dd5dd | ||
|
|
76e4635338 | ||
|
|
c97b88614f | ||
|
|
8738b13cd1 |
6
.github/workflows/docker-release.yml
vendored
6
.github/workflows/docker-release.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
- name: Set image tag
|
||||
id: tags
|
||||
run: |
|
||||
if [ ${{ github.event.action }} == "workflow_dispatch" ]; then
|
||||
if [ "${{ github.event.inputs.tags }}" != "" ]; then
|
||||
echo "::set-output name=tags::${{ github.event.inputs.tags }}"
|
||||
elif [ ${{ github.event.action }} == "release" ]; then
|
||||
echo "::set-output name=tags::${{ github.event.release.tag_name }}"
|
||||
@@ -53,8 +53,8 @@ jobs:
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}
|
||||
ghcr.io/${{ github.repository_owner }}/${{ secrets.DOCKERHUB_REPO }}
|
||||
tags: |
|
||||
raw=${{ steps.tags.outputs.tags }}
|
||||
raw=latest
|
||||
type=raw,value=${{ steps.tags.outputs.tags }}
|
||||
type=raw,value=latest
|
||||
|
||||
- name: setup platform emulator
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
3
.github/workflows/docker.yml
vendored
3
.github/workflows/docker.yml
vendored
@@ -13,6 +13,9 @@ on:
|
||||
- '**.pem'
|
||||
- '.dockerignore'
|
||||
- '.gitignore'
|
||||
schedule:
|
||||
- cron: '34 4 * * 2'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -9,15 +9,16 @@ RUN sh ./ffmpeg-fetch.sh
|
||||
# Create our Ubuntu 22.04 with node 16
|
||||
# Go to 20.04
|
||||
FROM ubuntu:20.04 AS base
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ENV UID=1000
|
||||
ENV GID=1000
|
||||
ENV USER=youtube
|
||||
ENV NO_UPDATE_NOTIFIER=true
|
||||
ENV PM2_HOME=/app/pm2
|
||||
ENV ALLOW_CONFIG_MUTATIONS=true
|
||||
RUN groupadd -g $GID $USER && useradd --system -m -g $USER --uid $UID $USER && \
|
||||
apt update && \
|
||||
apt install -y --no-install-recommends curl ca-certificates && \
|
||||
apt install -y --no-install-recommends curl ca-certificates tzdata && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && \
|
||||
apt install -y --no-install-recommends nodejs && \
|
||||
npm -g install npm && \
|
||||
@@ -49,18 +50,20 @@ RUN npm config set strict-ssl false && \
|
||||
FROM base
|
||||
RUN npm install -g pm2 && \
|
||||
apt update && \
|
||||
apt install -y --no-install-recommends gosu python3-minimal python-is-python3 atomicparsley && \
|
||||
apt install -y --no-install-recommends gosu python3-minimal python-is-python3 python3-pip atomicparsley && \
|
||||
apt clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
RUN pip install tcd
|
||||
WORKDIR /app
|
||||
# User 1000 already exist from base image
|
||||
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ]
|
||||
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffprobe", "/usr/local/bin/ffprobe" ]
|
||||
COPY --chown=$UID:$GID --from=backend ["/app/","/app/"]
|
||||
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
|
||||
RUN chmod +x /app/fix-scripts/*.sh
|
||||
# Add some persistence data
|
||||
#VOLUME ["/app/appdata"]
|
||||
|
||||
EXPOSE 17442
|
||||
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||
CMD [ "pm2-runtime","--raw","pm2.config.js" ]
|
||||
CMD [ "npm","start" ]
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
FROM tzahi12345/youtubedl-material:nightly
|
||||
CMD [ "pm2-runtime", "pm2.config.js" ]
|
||||
FROM tzahi12345/youtubedl-material:latest
|
||||
CMD [ "npm", "start" ]
|
||||
@@ -97,6 +97,11 @@ paths:
|
||||
summary: Get all files
|
||||
description: Gets all files and playlists stored in the db
|
||||
operationId: get-getAllFiles
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GetAllFilesRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
@@ -1724,6 +1729,41 @@ components:
|
||||
description: All video playlists
|
||||
items:
|
||||
$ref: '#/components/schemas/Playlist'
|
||||
GetAllFilesRequest:
|
||||
type: object
|
||||
properties:
|
||||
sort:
|
||||
$ref: '#/components/schemas/Sort'
|
||||
range:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
description: Two elements allowed, start index and end index
|
||||
minItems: 2
|
||||
maxItems: 2
|
||||
text_search:
|
||||
type: string
|
||||
description: Filter files by title
|
||||
file_type_filter:
|
||||
$ref: '#/components/schemas/FileTypeFilter'
|
||||
sub_id:
|
||||
type: string
|
||||
description: Include if you want to filter by subscription
|
||||
Sort:
|
||||
type: object
|
||||
properties:
|
||||
by:
|
||||
type: string
|
||||
description: Property to sort by
|
||||
order:
|
||||
type: number
|
||||
description: 1 for ascending, -1 for descending
|
||||
FileTypeFilter:
|
||||
type: string
|
||||
enum:
|
||||
- audio_only
|
||||
- video_only
|
||||
- both
|
||||
GetAllFilesResponse:
|
||||
required:
|
||||
- files
|
||||
@@ -1786,7 +1826,6 @@ components:
|
||||
required:
|
||||
- name
|
||||
- url
|
||||
- streamingOnly
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
@@ -1899,7 +1938,6 @@ components:
|
||||
- uids
|
||||
- playlistName
|
||||
- thumbnailURL
|
||||
- type
|
||||
type: object
|
||||
properties:
|
||||
playlistName:
|
||||
@@ -1908,8 +1946,6 @@ components:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
type:
|
||||
$ref: '#/components/schemas/FileType'
|
||||
thumbnailURL:
|
||||
type: string
|
||||
CreatePlaylistResponse:
|
||||
@@ -1939,15 +1975,17 @@ components:
|
||||
required:
|
||||
- playlist
|
||||
- success
|
||||
- type
|
||||
type: object
|
||||
properties:
|
||||
playlist:
|
||||
$ref: '#/components/schemas/Playlist'
|
||||
type:
|
||||
$ref: '#/components/schemas/FileType'
|
||||
success:
|
||||
type: boolean
|
||||
file_objs:
|
||||
type: array
|
||||
description: File objects for every uid in the playlist's uids property, in the same order
|
||||
items:
|
||||
$ref: '#/components/schemas/DatabaseFile'
|
||||
GetPlaylistsRequest:
|
||||
type: object
|
||||
properties:
|
||||
@@ -1972,13 +2010,10 @@ components:
|
||||
DeletePlaylistRequest:
|
||||
required:
|
||||
- playlist_id
|
||||
- type
|
||||
type: object
|
||||
properties:
|
||||
playlist_id:
|
||||
type: string
|
||||
type:
|
||||
$ref: '#/components/schemas/FileType'
|
||||
DownloadFileRequest:
|
||||
type: object
|
||||
properties:
|
||||
@@ -2385,6 +2420,16 @@ components:
|
||||
type: number
|
||||
local_view_count:
|
||||
type: number
|
||||
sub_id:
|
||||
type: string
|
||||
registered:
|
||||
type: number
|
||||
height:
|
||||
type: number
|
||||
description: In pixels, only for videos
|
||||
abr:
|
||||
type: number
|
||||
description: In Kbps
|
||||
Playlist:
|
||||
required:
|
||||
- uids
|
||||
@@ -2466,6 +2511,8 @@ components:
|
||||
type: string
|
||||
sub_name:
|
||||
type: string
|
||||
prefetched_info:
|
||||
type: object
|
||||
Task:
|
||||
required:
|
||||
- key
|
||||
@@ -2480,6 +2527,8 @@ components:
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
last_ran:
|
||||
type: number
|
||||
last_confirmed:
|
||||
@@ -2560,7 +2609,6 @@ components:
|
||||
- url
|
||||
- type
|
||||
- user_uid
|
||||
- streamingOnly
|
||||
- isPlaylist
|
||||
- videos
|
||||
type: object
|
||||
@@ -2576,8 +2624,6 @@ components:
|
||||
user_uid:
|
||||
type: string
|
||||
nullable: true
|
||||
streamingOnly:
|
||||
type: boolean
|
||||
isPlaylist:
|
||||
type: boolean
|
||||
archive:
|
||||
|
||||
21
README.md
21
README.md
@@ -12,16 +12,6 @@ Now with [Docker](#Docker) support!
|
||||
|
||||
<hr>
|
||||
|
||||
### USAGE OF THE NIGHTLY BUILDS IS HIGHLY RECOMMENDED.
|
||||
|
||||
For much better scaling with large datasets please run your YTDL-M instance with a MongoDB backend rather than the json file-based default.
|
||||
It will fix a lot of performance problems (especially with datasets in the tens of thousands videos/audios)!
|
||||
The (closed) issues as well as the project's Wiki will give you good starting points for your journey!
|
||||
|
||||
For MongoDB specifically there is [this little guide](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Setting-a-MongoDB-backend-to-use-as-database-provider-for-YTDL-M).
|
||||
|
||||
<hr>
|
||||
|
||||
## Getting Started
|
||||
|
||||
Check out the prerequisites, and go to the installation section. Easy as pie!
|
||||
@@ -58,6 +48,7 @@ sudo yum install nodejs youtube-dl ffmpeg ffmpeg-devel
|
||||
Optional dependencies:
|
||||
|
||||
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
|
||||
* [tcd](https://github.com/PetterKraabol/Twitch-Chat-Downloader) (for downloading Twitch VOD chats)
|
||||
|
||||
### Installing
|
||||
|
||||
@@ -91,7 +82,7 @@ Alternatively, you can port forward the port specified in the config (defaults t
|
||||
|
||||
### Host-specific instructions
|
||||
|
||||
If you're on a Synology NAS, unRAID or any other possible special case you can check if there's known issues or instructions both in the issue tracker and in the [Wiki!](https://github.com/Tzahi12345/YoutubeDL-Material/wiki#environment-specific-guideshelp)
|
||||
If you're on a Synology NAS, unRAID, Raspberry Pi 4 or any other possible special case you can check if there's known issues or instructions both in the issue tracker and in the [Wiki!](https://github.com/Tzahi12345/YoutubeDL-Material/wiki#environment-specific-guideshelp)
|
||||
|
||||
### Setup
|
||||
|
||||
@@ -102,8 +93,6 @@ If you are looking to setup YoutubeDL-Material with Docker, this section is for
|
||||
3. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 17443" or something similar. This tells you the *container-internal* port of the application. Please check your `docker-compose.yml` file for the *external* port. If you downloaded the file as described above, it defaults to **8998**.
|
||||
4. Make sure you can connect to the specified URL + *external* port, and if so, you are done!
|
||||
|
||||
NOTE: It is currently recommended that you use the `nightly` tag on Docker. To do so, simply update the docker-compose.yml `image` field so that it points to `tzahi12345/youtubedl-material:nightly`.
|
||||
|
||||
### Custom UID/GID
|
||||
|
||||
By default, the Docker container runs as non-root with UID=1000 and GID=1000. To set this to your own UID/GID, simply update the `environment` section in your `docker-compose.yml` like so:
|
||||
@@ -114,6 +103,12 @@ environment:
|
||||
GID: YOUR_GID
|
||||
```
|
||||
|
||||
## MongoDB
|
||||
|
||||
For much better scaling with large datasets please run your YoutubeDL-Material instance with MongoDB backend rather than the json file-based default. It will fix a lot of performance problems (especially with datasets in the tens of thousands videos/audios)!
|
||||
|
||||
[Tutorial](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Setting-a-MongoDB-backend-to-use-as-database-provider-for-YTDL-M).
|
||||
|
||||
## API
|
||||
|
||||
[API Docs](https://youtubedl-material.stoplight.io/docs/youtubedl-material/Public%20API%20v1.yaml)
|
||||
|
||||
18
SECURITY.md
18
SECURITY.md
@@ -2,16 +2,16 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Currently all work on this project goes into the nightly builds.
|
||||
4.2's RELEASE build is now quite old and should be considered legacy.
|
||||
We urge users to use the nightly releases, because the project
|
||||
constantly sees fixes.
|
||||
If you would like to see the latest updates, use the `nightly` tag on Docker.
|
||||
|
||||
| Version | Supported |
|
||||
| ------------- | ------------------ |
|
||||
| 4.2 Nightlies | :white_check_mark: |
|
||||
| 4.2 Release | :x: |
|
||||
| < 4.2 | :x: |
|
||||
If you'd like to stick with more stable releases, use the `latest` tag on Docker or download the [latest release here](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest).
|
||||
|
||||
| Version | Supported |
|
||||
| -------------------- | ------------------ |
|
||||
| 4.3 Docker Nightlies | :white_check_mark: |
|
||||
| 4.3 Release | :white_check_mark: |
|
||||
| 4.2 Release | :x: |
|
||||
| < 4.2 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "youtube-dl-material",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"prefix": "app",
|
||||
|
||||
@@ -68,7 +68,8 @@ db.defaults(
|
||||
configWriteFlag: false,
|
||||
downloads: {},
|
||||
subscriptions: [],
|
||||
files_to_db_migration_complete: false
|
||||
files_to_db_migration_complete: false,
|
||||
tasks_manager_role_migration_complete: false
|
||||
}).write();
|
||||
|
||||
users_db.defaults(
|
||||
@@ -101,7 +102,6 @@ let backendPort = null;
|
||||
let useDefaultDownloadingAgent = null;
|
||||
let customDownloadingAgent = null;
|
||||
let allowSubscriptions = null;
|
||||
let archivePath = path.join(__dirname, 'appdata', 'archives');
|
||||
|
||||
// other needed values
|
||||
let url_domain = null;
|
||||
@@ -148,16 +148,11 @@ if (fs.existsSync('version.json')) {
|
||||
|
||||
// don't overwrite config if it already happened.. NOT
|
||||
// let alreadyWritten = db.get('configWriteFlag').value();
|
||||
let writeConfigMode = process.env.write_ytdl_config;
|
||||
|
||||
// checks if config exists, if not, a config is auto generated
|
||||
config_api.configExistsCheck();
|
||||
|
||||
if (writeConfigMode) {
|
||||
setAndLoadConfig();
|
||||
} else {
|
||||
loadConfig();
|
||||
}
|
||||
setAndLoadConfig();
|
||||
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(bodyParser.json());
|
||||
@@ -188,13 +183,22 @@ async function checkMigrations() {
|
||||
if (!new_db_system_migration_complete) {
|
||||
logger.info('Beginning migration: 4.2->4.3+')
|
||||
let success = await db_api.importJSONToDB(db.value(), users_db.value());
|
||||
|
||||
await tasks_api.setupTasks(); // necessary as tasks were not properly initialized at first
|
||||
// sets migration to complete
|
||||
db.set('new_db_system_migration_complete', true).write();
|
||||
if (success) { logger.info('4.2->4.3+ migration complete!'); }
|
||||
else { logger.error('Migration failed: 4.2->4.3+'); }
|
||||
}
|
||||
|
||||
const tasks_manager_role_migration_complete = db.get('tasks_manager_role_migration_complete').value();
|
||||
if (!tasks_manager_role_migration_complete) {
|
||||
logger.info('Checking if tasks manager role permissions exist for admin user...');
|
||||
const success = await auth_api.changeRolePermissions('admin', 'tasks_manager', 'yes');
|
||||
if (success) logger.info('Task manager permissions check complete!');
|
||||
else logger.error('Failed to auto add tasks manager permissions to admin role!');
|
||||
db.set('tasks_manager_role_migration_complete', true).write();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -484,8 +488,9 @@ async function setAndLoadConfig() {
|
||||
}
|
||||
|
||||
async function setConfigFromEnv() {
|
||||
let config_items = getEnvConfigItems();
|
||||
let success = config_api.setConfigItems(config_items);
|
||||
const config_items = getEnvConfigItems();
|
||||
if (!config_items || config_items.length === 0) return true;
|
||||
const success = config_api.setConfigItems(config_items);
|
||||
if (success) {
|
||||
logger.info('Config items set using ENV variables.');
|
||||
await utils.wait(100);
|
||||
@@ -500,12 +505,13 @@ async function loadConfig() {
|
||||
loadConfigValues();
|
||||
|
||||
// connect to DB
|
||||
await db_api.connectToDB();
|
||||
if (!config_api.getConfigItem('ytdl_use_local_db'))
|
||||
await db_api.connectToDB();
|
||||
db_api.database_initialized = true;
|
||||
db_api.database_initialized_bs.next(true);
|
||||
|
||||
// creates archive path if missing
|
||||
await fs.ensureDir(archivePath);
|
||||
await fs.ensureDir(utils.getArchiveFolder());
|
||||
|
||||
// check migrations
|
||||
await checkMigrations();
|
||||
@@ -575,7 +581,11 @@ async function watchSubscriptions() {
|
||||
|
||||
if (!subscriptions) return;
|
||||
|
||||
const valid_subscriptions = subscriptions.filter(sub => !sub.paused);
|
||||
// auto pause deprecated streamingOnly mode
|
||||
const streaming_only_subs = subscriptions.filter(sub => sub.streamingOnly);
|
||||
subscriptions_api.updateSubscriptionPropertyMultiple(streaming_only_subs, {paused: true});
|
||||
|
||||
const valid_subscriptions = subscriptions.filter(sub => !sub.paused && !sub.streamingOnly);
|
||||
|
||||
let subscriptions_amount = valid_subscriptions.length;
|
||||
let delay_interval = calculateSubcriptionRetrievalDelay(subscriptions_amount);
|
||||
@@ -912,11 +922,11 @@ app.post('/api/getFile', optionalJwt, async function (req, res) {
|
||||
app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
|
||||
// these are returned
|
||||
let files = null;
|
||||
let playlists = null;
|
||||
let sort = req.body.sort;
|
||||
let range = req.body.range;
|
||||
let text_search = req.body.text_search;
|
||||
let file_type_filter = req.body.file_type_filter;
|
||||
const sort = req.body.sort;
|
||||
const range = req.body.range;
|
||||
const text_search = req.body.text_search;
|
||||
const file_type_filter = req.body.file_type_filter;
|
||||
const sub_id = req.body.sub_id;
|
||||
const uuid = req.isAuthenticated() ? req.user.uid : null;
|
||||
|
||||
const filter_obj = {user_uid: uuid};
|
||||
@@ -929,6 +939,10 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
if (sub_id) {
|
||||
filter_obj['sub_id'] = sub_id;
|
||||
}
|
||||
|
||||
if (file_type_filter === 'audio_only') filter_obj['isAudio'] = true;
|
||||
else if (file_type_filter === 'video_only') filter_obj['isAudio'] = false;
|
||||
|
||||
@@ -1268,7 +1282,7 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
|
||||
subscription = JSON.parse(JSON.stringify(subscription));
|
||||
|
||||
// get sub videos
|
||||
if (subscription.name && !subscription.streamingOnly) {
|
||||
if (subscription.name) {
|
||||
var parsed_files = await db_api.getRecords('files', {sub_id: subscription.id}); // subscription.videos;
|
||||
subscription['videos'] = parsed_files;
|
||||
// loop through files for extra processing
|
||||
@@ -1278,19 +1292,6 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
|
||||
if (file && file['url'].includes('twitch.tv')) file['chat_exists'] = fs.existsSync(file['path'].substring(0, file['path'].length - 4) + '.twitch_chat.json');
|
||||
}
|
||||
|
||||
res.send({
|
||||
subscription: subscription,
|
||||
files: parsed_files
|
||||
});
|
||||
} else if (subscription.name && subscription.streamingOnly) {
|
||||
// return list of videos
|
||||
let parsed_files = [];
|
||||
if (subscription.videos) {
|
||||
for (let i = 0; i < subscription.videos.length; i++) {
|
||||
const video = subscription.videos[i];
|
||||
parsed_files.push(new utils.File(video.title, video.title, video.thumbnail, false, video.duration, video.url, video.uploader, video.size, null, null, video.upload_date, video.view_count, video.height, video.abr));
|
||||
}
|
||||
}
|
||||
res.send({
|
||||
subscription: subscription,
|
||||
files: parsed_files
|
||||
@@ -1335,9 +1336,8 @@ app.post('/api/getSubscriptions', optionalJwt, async (req, res) => {
|
||||
app.post('/api/createPlaylist', optionalJwt, async (req, res) => {
|
||||
let playlistName = req.body.playlistName;
|
||||
let uids = req.body.uids;
|
||||
let type = req.body.type;
|
||||
|
||||
const new_playlist = await db_api.createPlaylist(playlistName, uids, type, req.isAuthenticated() ? req.user.uid : null);
|
||||
const new_playlist = await db_api.createPlaylist(playlistName, uids, req.isAuthenticated() ? req.user.uid : null);
|
||||
|
||||
res.send({
|
||||
new_playlist: new_playlist,
|
||||
@@ -1365,7 +1365,6 @@ app.post('/api/getPlaylist', optionalJwt, async (req, res) => {
|
||||
res.send({
|
||||
playlist: playlist,
|
||||
file_objs: file_objs,
|
||||
type: playlist && playlist.type,
|
||||
success: !!playlist
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": "",
|
||||
"use_twitch_API": false,
|
||||
"twitch_API_key": "",
|
||||
"twitch_client_ID": "",
|
||||
"twitch_client_secret": "",
|
||||
"twitch_auto_download_chat": false,
|
||||
"use_sponsorblock_API": false,
|
||||
"generate_NFO_files": false
|
||||
@@ -63,7 +64,7 @@
|
||||
"mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib"
|
||||
},
|
||||
"Advanced": {
|
||||
"default_downloader": "youtube-dl",
|
||||
"default_downloader": "yt-dlp",
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
"multi_user_mode": false,
|
||||
|
||||
@@ -361,7 +361,6 @@ exports.userHasPermission = async function(user_uid, permission) {
|
||||
logger.error('Invalid role ' + role);
|
||||
return false;
|
||||
}
|
||||
const role_permissions = (await db_api.getRecords('roles'))['permissions'];
|
||||
|
||||
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
|
||||
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
|
||||
@@ -376,7 +375,8 @@ exports.userHasPermission = async function(user_uid, permission) {
|
||||
}
|
||||
|
||||
// no overrides, let's check if the role has the permission
|
||||
if (role_permissions.includes(permission)) {
|
||||
const role_has_permission = await exports.roleHasPermissions(role, permission);
|
||||
if (role_has_permission) {
|
||||
return true;
|
||||
} else {
|
||||
logger.verbose(`User ${user_uid} failed to get permission ${permission}`);
|
||||
@@ -384,6 +384,16 @@ exports.userHasPermission = async function(user_uid, permission) {
|
||||
}
|
||||
}
|
||||
|
||||
exports.roleHasPermissions = async function(role, permission) {
|
||||
const role_obj = await db_api.getRecord('roles', {key: role})
|
||||
if (!role) {
|
||||
logger.error(`Role ${role} does not exist!`);
|
||||
}
|
||||
const role_permissions = role_obj['permissions'];
|
||||
if (role_permissions && role_permissions.includes(permission)) return true;
|
||||
else return false;
|
||||
}
|
||||
|
||||
exports.userPermissions = async function(user_uid) {
|
||||
let user_permissions = [];
|
||||
const user_obj = await db_api.getRecord('users', ({uid: user_uid}));
|
||||
|
||||
@@ -206,7 +206,8 @@ const DEFAULT_CONFIG = {
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": "",
|
||||
"use_twitch_API": false,
|
||||
"twitch_API_key": "",
|
||||
"twitch_client_ID": "",
|
||||
"twitch_client_secret": "",
|
||||
"twitch_auto_download_chat": false,
|
||||
"use_sponsorblock_API": false,
|
||||
"generate_NFO_files": false
|
||||
@@ -238,7 +239,7 @@ const DEFAULT_CONFIG = {
|
||||
"mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib"
|
||||
},
|
||||
"Advanced": {
|
||||
"default_downloader": "youtube-dl",
|
||||
"default_downloader": "yt-dlp",
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
"multi_user_mode": false,
|
||||
|
||||
@@ -102,9 +102,13 @@ exports.CONFIG_ITEMS = {
|
||||
'key': 'ytdl_use_twitch_api',
|
||||
'path': 'YoutubeDLMaterial.API.use_twitch_API'
|
||||
},
|
||||
'ytdl_twitch_api_key': {
|
||||
'key': 'ytdl_twitch_api_key',
|
||||
'path': 'YoutubeDLMaterial.API.twitch_API_key'
|
||||
'ytdl_twitch_client_id': {
|
||||
'key': 'ytdl_twitch_client_id',
|
||||
'path': 'YoutubeDLMaterial.API.twitch_client_ID'
|
||||
},
|
||||
'ytdl_twitch_client_secret': {
|
||||
'key': 'ytdl_twitch_client_secret',
|
||||
'path': 'YoutubeDLMaterial.API.twitch_client_secret'
|
||||
},
|
||||
'ytdl_twitch_auto_download_chat': {
|
||||
'key': 'ytdl_twitch_auto_download_chat',
|
||||
@@ -217,7 +221,8 @@ exports.AVAILABLE_PERMISSIONS = [
|
||||
'subscriptions',
|
||||
'sharing',
|
||||
'advanced_download',
|
||||
'downloads_manager'
|
||||
'downloads_manager',
|
||||
'tasks_manager'
|
||||
];
|
||||
|
||||
exports.DETAILS_BIN_PATH = 'node_modules/youtube-dl/bin/details'
|
||||
@@ -301,4 +306,4 @@ const YTDL_ARGS_WITH_VALUES = [
|
||||
// we're using a Set here for performance
|
||||
exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES);
|
||||
|
||||
exports.CURRENT_VERSION = 'v4.2';
|
||||
exports.CURRENT_VERSION = 'v4.3';
|
||||
|
||||
@@ -198,7 +198,7 @@ async function registerFileDBManual(file_object) {
|
||||
path_object = path.parse(file_object['path']);
|
||||
file_object['path'] = path.format(path_object);
|
||||
|
||||
exports.insertRecordIntoTable('files', file_object, {path: file_object['path']})
|
||||
await exports.insertRecordIntoTable('files', file_object, {path: file_object['path']})
|
||||
|
||||
return file_object;
|
||||
}
|
||||
@@ -357,7 +357,7 @@ exports.addMetadataPropertyToDB = async (property_key) => {
|
||||
}
|
||||
}
|
||||
|
||||
exports.createPlaylist = async (playlist_name, uids, type, user_uid = null) => {
|
||||
exports.createPlaylist = async (playlist_name, uids, user_uid = null) => {
|
||||
const first_video = await exports.getVideo(uids[0]);
|
||||
const thumbnailToUse = first_video['thumbnailURL'];
|
||||
|
||||
@@ -366,7 +366,6 @@ exports.createPlaylist = async (playlist_name, uids, type, user_uid = null) => {
|
||||
uids: uids,
|
||||
id: uuid(),
|
||||
thumbnailURL: thumbnailToUse,
|
||||
type: type,
|
||||
registered: Date.now(),
|
||||
randomize_order: false
|
||||
};
|
||||
@@ -495,8 +494,7 @@ exports.deleteFile = async (uid, uuid = null, blacklistMode = false) => {
|
||||
|
||||
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||
if (useYoutubeDLArchive) {
|
||||
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||
const archive_path = uuid ? path.join(usersFileFolder, uuid, 'archives', `archive_${type}.txt`) : path.join('appdata', 'archives', `archive_${type}.txt`);
|
||||
const archive_path = utils.getArchiveFolder(type, uuid);
|
||||
|
||||
// get ID from JSON
|
||||
|
||||
@@ -504,14 +502,8 @@ exports.deleteFile = async (uid, uuid = null, blacklistMode = false) => {
|
||||
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'));
|
||||
}
|
||||
// Remove file ID from the archive file, and write it to the blacklist (if enabled)
|
||||
await utils.deleteFileFromArchive(uid, type, archive_path, id, blacklistMode);
|
||||
}
|
||||
|
||||
if (jsonExists) await fs.unlink(jsonPath);
|
||||
@@ -933,6 +925,7 @@ exports.importJSONToDB = async (db_json, users_json) => {
|
||||
const createFilesRecords = (files, subscriptions) => {
|
||||
for (let i = 0; i < subscriptions.length; i++) {
|
||||
const subscription = subscriptions[i];
|
||||
if (!subscription['videos']) continue;
|
||||
subscription['videos'] = subscription['videos'].map(file => ({ ...file, sub_id: subscription['id'], user_uid: subscription['user_uid'] ? subscription['user_uid'] : undefined}));
|
||||
files = files.concat(subscriptions[i]['videos']);
|
||||
}
|
||||
@@ -993,7 +986,7 @@ exports.backupDB = async () => {
|
||||
const backup_file_name = `${using_local_db ? 'local' : 'remote'}_db.json.${Date.now()/1000}.bak`;
|
||||
const path_to_backups = path.join(backup_dir, backup_file_name);
|
||||
|
||||
logger.verbose(`Backing up ${using_local_db ? 'local' : 'remote'} DB to ${path_to_backups}`);
|
||||
logger.info(`Backing up ${using_local_db ? 'local' : 'remote'} DB to ${path_to_backups}`);
|
||||
|
||||
const table_to_records = {};
|
||||
for (let i = 0; i < tables_list.length; i++) {
|
||||
@@ -1040,10 +1033,11 @@ exports.transferDB = async (local_to_remote) => {
|
||||
table_to_records[table] = await exports.getRecords(table);
|
||||
}
|
||||
|
||||
logger.info('Backup up DB...');
|
||||
await exports.backupDB(); // should backup always
|
||||
|
||||
using_local_db = !local_to_remote;
|
||||
if (local_to_remote) {
|
||||
logger.debug('Backup up DB...');
|
||||
await exports.backupDB();
|
||||
const db_connected = await exports.connectToDB(5, true);
|
||||
if (!db_connected) {
|
||||
logger.error('Failed to transfer database - could not connect to MongoDB. Verify that your connection URL is valid.');
|
||||
@@ -1111,15 +1105,3 @@ exports.applyFilterLocalDB = (db_path, filter_obj, operation) => {
|
||||
});
|
||||
return return_val;
|
||||
}
|
||||
|
||||
// archive helper functions
|
||||
|
||||
async function writeToBlacklist(type, line) {
|
||||
const archivePath = path.join(__dirname, 'appdata', 'archives');
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@ const db_api = require('./db');
|
||||
const mutex = new Mutex();
|
||||
let should_check_downloads = true;
|
||||
|
||||
const archivePath = path.join(__dirname, 'appdata', 'archives');
|
||||
|
||||
if (db_api.database_initialized) {
|
||||
setupDownloads();
|
||||
} else {
|
||||
@@ -28,7 +26,7 @@ if (db_api.database_initialized) {
|
||||
});
|
||||
}
|
||||
|
||||
exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null) => {
|
||||
exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null, prefetched_info = null) => {
|
||||
return await mutex.runExclusive(async () => {
|
||||
const download = {
|
||||
url: url,
|
||||
@@ -37,6 +35,7 @@ exports.createDownload = async (url, type, options, user_uid = null, sub_id = nu
|
||||
user_uid: user_uid,
|
||||
sub_id: sub_id,
|
||||
sub_name: sub_name,
|
||||
prefetched_info: prefetched_info,
|
||||
options: options,
|
||||
uid: uuid(),
|
||||
step_index: 0,
|
||||
@@ -187,7 +186,7 @@ async function collectInfo(download_uid) {
|
||||
let args = await exports.generateArgs(url, type, options, download['user_uid']);
|
||||
|
||||
// get video info prior to download
|
||||
let info = await exports.getVideoInfoByURL(url, args, download_uid);
|
||||
let info = download['prefetched_info'] ? download['prefetched_info'] : await exports.getVideoInfoByURL(url, args, download_uid);
|
||||
|
||||
if (!info) {
|
||||
// info failed, error presumably already recorded
|
||||
@@ -204,6 +203,7 @@ async function collectInfo(download_uid) {
|
||||
options.customOutput = category['custom_output'];
|
||||
options.noRelativePath = true;
|
||||
args = await exports.generateArgs(url, type, options, download['user_uid']);
|
||||
args = utils.filterArgs(args, ['--no-simulate']);
|
||||
info = await exports.getVideoInfoByURL(url, args, download_uid);
|
||||
}
|
||||
|
||||
@@ -229,7 +229,8 @@ async function collectInfo(download_uid) {
|
||||
options: options,
|
||||
files_to_check_for_progress: files_to_check_for_progress,
|
||||
expected_file_size: expected_file_size,
|
||||
title: playlist_title ? playlist_title : info['title']
|
||||
title: playlist_title ? playlist_title : info['title'],
|
||||
prefetched_info: null
|
||||
});
|
||||
}
|
||||
|
||||
@@ -242,6 +243,7 @@ async function downloadQueuedFile(download_uid) {
|
||||
return new Promise(async resolve => {
|
||||
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
||||
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
|
||||
const usersFolderPath = config_api.getConfigItem('ytdl_users_base_path');
|
||||
await db_api.updateRecord('download_queue', {uid: download_uid}, {step_index: 2, finished_step: false, running: true});
|
||||
|
||||
const url = download['url'];
|
||||
@@ -249,9 +251,11 @@ async function downloadQueuedFile(download_uid) {
|
||||
const options = download['options'];
|
||||
const args = download['args'];
|
||||
const category = download['category'];
|
||||
let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; // TODO: fix
|
||||
let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath;
|
||||
if (options.customFileFolderPath) {
|
||||
fileFolderPath = options.customFileFolderPath;
|
||||
} else if (download['user_uid']) {
|
||||
fileFolderPath = path.join(usersFolderPath, download['user_uid'], type);
|
||||
}
|
||||
fs.ensureDirSync(fileFolderPath);
|
||||
|
||||
@@ -353,7 +357,7 @@ async function downloadQueuedFile(download_uid) {
|
||||
if (file_objs.length > 1) {
|
||||
// create playlist
|
||||
const playlist_name = file_objs.map(file_obj => file_obj.title).join(', ');
|
||||
container = await db_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), type, download['user_uid']);
|
||||
container = await db_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), download['user_uid']);
|
||||
} else if (file_objs.length === 1) {
|
||||
container = file_objs[0];
|
||||
} else {
|
||||
@@ -373,15 +377,23 @@ async function downloadQueuedFile(download_uid) {
|
||||
// helper functions
|
||||
|
||||
exports.generateArgs = async (url, type, options, user_uid = null, simulated = false) => {
|
||||
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
|
||||
|
||||
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
||||
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
|
||||
const usersFolderPath = config_api.getConfigItem('ytdl_users_base_path');
|
||||
|
||||
const videopath = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
|
||||
const globalArgs = config_api.getConfigItem('ytdl_custom_args');
|
||||
const useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
||||
const is_audio = type === 'audio';
|
||||
|
||||
let fileFolderPath = is_audio ? audioFolderPath : videoFolderPath;
|
||||
let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; // TODO: fix
|
||||
if (options.customFileFolderPath) {
|
||||
fileFolderPath = options.customFileFolderPath;
|
||||
} else if (user_uid) {
|
||||
fileFolderPath = path.join(usersFolderPath, user_uid, fileFolderPath);
|
||||
}
|
||||
|
||||
if (options.customFileFolderPath) fileFolderPath = options.customFileFolderPath;
|
||||
|
||||
@@ -391,6 +403,8 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
|
||||
|
||||
// video-specific args
|
||||
const selectedHeight = options.selectedHeight;
|
||||
const maxHeight = options.maxHeight;
|
||||
const heightParam = selectedHeight || maxHeight;
|
||||
|
||||
// audio-specific args
|
||||
const maxBitrate = options.maxBitrate;
|
||||
@@ -404,8 +418,6 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
|
||||
if (!is_audio && !is_youtube) {
|
||||
// tiktok videos fail when using the default format
|
||||
qualityPath = null;
|
||||
} else if (!is_audio && !is_youtube && (url.includes('reddit') || url.includes('pornhub'))) {
|
||||
qualityPath = ['-f', 'bestvideo+bestaudio']
|
||||
}
|
||||
|
||||
if (customArgs) {
|
||||
@@ -413,8 +425,8 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
|
||||
} else {
|
||||
if (customQualityConfiguration) {
|
||||
qualityPath = ['-f', customQualityConfiguration, '--merge-output-format', 'mp4'];
|
||||
} else if (selectedHeight && selectedHeight !== '' && !is_audio) {
|
||||
qualityPath = ['-f', `'(mp4)[height=${selectedHeight}'`];
|
||||
} else if (heightParam && heightParam !== '' && !is_audio) {
|
||||
qualityPath = ['-f', `'(mp4)[height${maxHeight ? '<' : ''}=${heightParam}]`];
|
||||
} else if (is_audio) {
|
||||
qualityPath = ['--audio-quality', maxBitrate ? maxBitrate : '0']
|
||||
}
|
||||
@@ -496,9 +508,11 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
|
||||
downloadConfig.push('-r', rate_limit);
|
||||
}
|
||||
|
||||
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
|
||||
if (default_downloader === 'yt-dlp') {
|
||||
downloadConfig.push('--no-clean-infojson');
|
||||
downloadConfig = utils.filterArgs(downloadConfig, ['--print-json']);
|
||||
|
||||
// in yt-dlp -j --no-simulate is preferable
|
||||
downloadConfig.push('--no-clean-info-json', '-j', '--no-simulate');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -506,7 +520,7 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
|
||||
// filter out incompatible args
|
||||
downloadConfig = filterArgs(downloadConfig, is_audio);
|
||||
|
||||
if (!simulated) logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`);
|
||||
if (!simulated) logger.verbose(`${default_downloader} args being used: ${downloadConfig.join(',')}`);
|
||||
return downloadConfig;
|
||||
}
|
||||
|
||||
@@ -565,8 +579,7 @@ exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
|
||||
function filterArgs(args, isAudio) {
|
||||
const video_only_args = ['--add-metadata', '--embed-subs', '--xattrs'];
|
||||
const audio_only_args = ['-x', '--extract-audio', '--embed-thumbnail'];
|
||||
const args_to_remove = isAudio ? video_only_args : audio_only_args;
|
||||
return args.filter(x => !args_to_remove.includes(x));
|
||||
return utils.filterArgs(args, isAudio ? video_only_args : audio_only_args);
|
||||
}
|
||||
|
||||
async function checkDownloadPercent(download_uid) {
|
||||
@@ -628,6 +641,6 @@ function getArchiveFolder(fileFolderPath, options, user_uid) {
|
||||
} else if (user_uid) {
|
||||
return path.join(fileFolderPath, 'archives');
|
||||
} else {
|
||||
return path.join(archivePath);
|
||||
return path.join('appdata', 'archives');
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
CMD="pm2-runtime pm2.config.js"
|
||||
CMD="npm start"
|
||||
|
||||
# if the first arg starts with "-" pass it to program
|
||||
if [ "${1#-}" != "$1" ]; then
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
# INTERACTIVE PERMISSIONS FIX SCRIPT FOR YTDL-M
|
||||
# Date: 2022-05-03
|
||||
@@ -6,8 +6,7 @@
|
||||
# If you want to run this script on a bare-metal installation instead of within Docker
|
||||
# make sure that the paths configured below match your paths! (it's wise to use the full paths)
|
||||
# USAGE: within your container's bash shell:
|
||||
# chmod -R +x ./fix-scripts/
|
||||
# ./fix-scripts/001-fix_download_permissions.sh
|
||||
# ./fix-scripts/<name of fix-script>
|
||||
|
||||
# User defines / Docker env defaults
|
||||
PATH_SUBS=/app/subscriptions
|
||||
|
||||
142
backend/fix-scripts/002-fix_dupes_per_archive_file.sh
Normal file
142
backend/fix-scripts/002-fix_dupes_per_archive_file.sh
Normal file
@@ -0,0 +1,142 @@
|
||||
#!/bin/bash
|
||||
|
||||
# INTERACTIVE ARCHIVE-DUPE-ENTRY FIX SCRIPT FOR YTDL-M
|
||||
# Date: 2022-05-09
|
||||
|
||||
# If you want to run this script on a bare-metal installation instead of within Docker
|
||||
# make sure that the paths configured below match your paths! (it's wise to use the full paths)
|
||||
# USAGE: within your container's bash shell:
|
||||
# ./fix-scripts/<name of fix-script>
|
||||
|
||||
# User defines (NO TRAILING SLASHES) / Docker env defaults
|
||||
PATH_SUBSARCHIVE=/app/subscriptions/archives
|
||||
PATH_ONEOFFARCHIVE=/app/appdata/archives
|
||||
|
||||
# Backup paths (substitute with your personal preference if you like)
|
||||
PATH_SUBSARCHIVEBKP=$PATH_SUBSARCHIVE-BKP-$(date +%Y%m%d%H%M%S)
|
||||
PATH_ONEOFFARCHIVEBKP=$PATH_ONEOFFARCHIVE-BKP-$(date +%Y%m%d%H%M%S)
|
||||
|
||||
|
||||
# Define Colors for TUI
|
||||
yellow=$(tput setaf 3)
|
||||
normal=$(tput sgr0)
|
||||
|
||||
tput civis # hide the cursor
|
||||
|
||||
clear -x
|
||||
printf "\n"
|
||||
printf '%*s\n' "${COLUMNS:-$(tput cols)}" '' | tr ' ' - # horizontal line
|
||||
printf "Welcome to the INTERACTIVE ARCHIVE-DUPE-ENTRY FIX SCRIPT FOR YTDL-M."
|
||||
printf "\nThis script will cycle through the archive files in the folders mentioned"
|
||||
printf "\nbelow and remove within each archive the dupe entries. (compact them)"
|
||||
printf "\nDuring some older builds of YTDL-M the archives could receive dupe"
|
||||
printf "\nentries and blow up in size, sometimes causing conflicts with download management."
|
||||
printf '\n%*s' "${COLUMNS:-$(tput cols)}" '' | tr ' ' - # horizontal line
|
||||
printf "\n"
|
||||
|
||||
# check whether dirs exist
|
||||
i=0
|
||||
[ -d $PATH_SUBSARCHIVE ] && i=$((i+1)) && printf "\n✔ (${i}/2) Found Subscriptions archive directory at ${PATH_SUBSARCHIVE}"
|
||||
[ -d $PATH_ONEOFFARCHIVE ] && i=$((i+1)) && printf "\n✔ (${i}/2) Found one-off archive directory at ${PATH_ONEOFFARCHIVE}"
|
||||
|
||||
# Ask to proceed or cancel, exit on missing paths
|
||||
case $i in
|
||||
0)
|
||||
printf "\n\n Couldn't find any archive location path! \n\nPlease edit this script to configure!"
|
||||
tput cnorm
|
||||
exit 2;;
|
||||
2)
|
||||
printf "\n\n Found all archive locations. \n\nProceed? (Y/N)";;
|
||||
*)
|
||||
printf "\n\n Only found ${i} out of 2 archive locations! Something about this script's config must be wrong. \n\nProceed anyways? (Y/N)";;
|
||||
esac
|
||||
old_stty_cfg=$(stty -g)
|
||||
stty raw -echo ; answer=$(head -c 1) ; stty $old_stty_cfg # Careful playing with stty
|
||||
if echo "$answer" | grep -iq "^y" ;then
|
||||
printf "\n\nRunning jobs now... (this may take a while)\n"
|
||||
|
||||
printf "\nBacking up directories...\n"
|
||||
|
||||
chars="⣾⣽⣻⢿⡿⣟⣯⣷"
|
||||
cp -R $PATH_SUBSARCHIVE $PATH_SUBSARCHIVEBKP &
|
||||
PID=$!
|
||||
i=1
|
||||
echo -n ' '
|
||||
while [ -d /proc/$PID ]
|
||||
do
|
||||
printf "${yellow}\b${chars:i++%${#chars}:1}${normal}"
|
||||
sleep 0.15
|
||||
done
|
||||
[ -d $PATH_SUBSARCHIVEBKP ] && printf "\r✔ Backed up ${PATH_SUBSARCHIVE} to ${PATH_SUBSARCHIVEBKP} ($(du -sh $PATH_SUBSARCHIVEBKP | cut -f1))\n"
|
||||
|
||||
cp -R $PATH_ONEOFFARCHIVE $PATH_ONEOFFARCHIVEBKP &
|
||||
PID2=$!
|
||||
i=1
|
||||
echo -n ' '
|
||||
while [ -d /proc/$PID2 ]
|
||||
do
|
||||
printf "${yellow}\b${chars:i++%${#chars}:1}${normal}"
|
||||
sleep 0.1
|
||||
done
|
||||
[ -d $PATH_ONEOFFARCHIVEBKP ] && printf "\r✔ Backed up ${PATH_ONEOFFARCHIVE} to ${PATH_ONEOFFARCHIVEBKP} ($(du -sh $PATH_ONEOFFARCHIVEBKP | cut -f1))\n"
|
||||
|
||||
|
||||
printf "\nCompacting files...\n"
|
||||
|
||||
tmpfile=$(mktemp) &&
|
||||
|
||||
[ -d $PATH_SUBSARCHIVE ] &&
|
||||
find $PATH_SUBSARCHIVE -name '*.txt' -print0 | while read -d $'\0' file # Set delimiter to null because we want to catch all possible filenames (WE CANNOT CHANGE IFS HERE) - https://stackoverflow.com/a/15931055
|
||||
do
|
||||
cp "$file" "$tmpfile"
|
||||
{ awk '!x[$0]++' "$tmpfile" > "$file"; } & # https://unix.stackexchange.com/questions/159695/how-does-awk-a0-work
|
||||
PID3=$!
|
||||
i=1
|
||||
echo -n ''
|
||||
while [ -d /proc/$PID3 ]
|
||||
do
|
||||
printf "${yellow}\b${chars:i++%${#chars}:1}${normal}"
|
||||
sleep 0.1
|
||||
done
|
||||
BEFORE=$(wc -l < $tmpfile)
|
||||
AFTER=$(wc -l < $file)
|
||||
if [[ "$AFTER" -ne "$BEFORE" ]]; then
|
||||
printf "\b✔ Compacted down to ${AFTER} lines from ${BEFORE}: ${file}\n"
|
||||
else
|
||||
printf "\bℹ No action needed for file: ${file}\n"
|
||||
fi
|
||||
done
|
||||
|
||||
[ -d $PATH_ONEOFFARCHIVE ] &&
|
||||
find $PATH_ONEOFFARCHIVE -name '*.txt' -print0 | while read -d $'\0' file
|
||||
do
|
||||
cp "$file" "$tmpfile" &
|
||||
awk '!x[$0]++' "$tmpfile" > "$file" &
|
||||
PID4=$!
|
||||
i=1
|
||||
echo -n ''
|
||||
while [ -d /proc/$PID4 ]
|
||||
do
|
||||
printf "${yellow}\b${chars:i++%${#chars}:1}${normal}"
|
||||
sleep 0.1
|
||||
done
|
||||
BEFORE=$(wc -l < $tmpfile)
|
||||
AFTER=$(wc -l < $file)
|
||||
if [ "$BEFORE" -ne "$AFTER" ]; then
|
||||
printf "\b✔ Compacted down to ${AFTER} lines from ${BEFORE}: ${file}\n"
|
||||
else
|
||||
printf "\bℹ No action ran for file: ${file}\n"
|
||||
fi
|
||||
done
|
||||
tput cnorm # show the cursor
|
||||
rm "$tmpfile"
|
||||
|
||||
printf "\n\n✔ Done."
|
||||
printf "\nℹ Please keep in mind that you may still want to"
|
||||
printf "\n run corruption checks against your archives!\n\n"
|
||||
exit
|
||||
else
|
||||
tput cnorm
|
||||
printf "\nOkay, bye.\n\n"
|
||||
exit
|
||||
fi
|
||||
1825
backend/package-lock.json
generated
1825
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,20 +5,9 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "nodemon app.js",
|
||||
"start": "pm2-runtime --raw pm2.config.js",
|
||||
"debug": "set YTDL_MODE=debug && node app.js"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
"*.js",
|
||||
"appdata/*",
|
||||
"public/*"
|
||||
],
|
||||
"watch": [
|
||||
"restart_update.json",
|
||||
"restart_general.json"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": ""
|
||||
@@ -47,11 +36,10 @@
|
||||
"mocha": "^9.2.2",
|
||||
"moment": "^2.29.2",
|
||||
"mongodb": "^3.6.9",
|
||||
"multer": "^1.4.2",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-id3": "^0.1.14",
|
||||
"node-schedule": "^2.1.0",
|
||||
"nodemon": "^2.0.7",
|
||||
"passport": "^0.4.1",
|
||||
"passport-http": "^0.3.0",
|
||||
"passport-jwt": "^4.0.0",
|
||||
|
||||
@@ -6,4 +6,4 @@ module.exports = {
|
||||
out_file: "/dev/null",
|
||||
error_file: "/dev/null"
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
|
||||
]);
|
||||
|
||||
if (jsonExists) {
|
||||
retrievedID = JSON.parse(await fs.readFile(jsonPath, 'utf8'))['id'];
|
||||
retrievedID = fs.readJSONSync(jsonPath)['id'];
|
||||
await fs.unlink(jsonPath);
|
||||
}
|
||||
|
||||
@@ -196,12 +196,11 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
|
||||
return false;
|
||||
} else {
|
||||
// check if the user wants the video to be redownloaded (deleteForever === false)
|
||||
if (!deleteForever && useArchive && sub.archive && retrievedID) {
|
||||
const archive_path = path.join(sub.archive, 'archive.txt')
|
||||
// if archive exists, remove line with video ID
|
||||
if (await fs.pathExists(archive_path)) {
|
||||
utils.removeIDFromArchive(archive_path, retrievedID);
|
||||
}
|
||||
if (useArchive && retrievedID) {
|
||||
const archive_path = utils.getArchiveFolder(sub.type, user_uid, sub);
|
||||
|
||||
// Remove file ID from the archive file, and write it to the blacklist (if enabled)
|
||||
await utils.deleteFileFromArchive(file_uid, sub.type, archive_path, retrievedID, deleteForever);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -242,64 +241,22 @@ async function getVideosForSub(sub, user_uid = null) {
|
||||
logger.verbose('Subscription: finished check for ' + sub.name);
|
||||
if (err && !output) {
|
||||
logger.error(err.stderr ? err.stderr : err.message);
|
||||
if (err.stderr.includes('This video is unavailable')) {
|
||||
if (err.stderr.includes('This video is unavailable') || err.stderr.includes('Private video')) {
|
||||
logger.info('An error was encountered with at least one video, backup method will be used.')
|
||||
try {
|
||||
// TODO: reimplement
|
||||
|
||||
// const outputs = err.stdout.split(/\r\n|\r|\n/);
|
||||
// for (let i = 0; i < outputs.length; i++) {
|
||||
// const output = JSON.parse(outputs[i]);
|
||||
// await handleOutputJSON(sub, output, i === 0, multiUserMode)
|
||||
// if (err.stderr.includes(output['id']) && archive_path) {
|
||||
// // we found a video that errored! add it to the archive to prevent future errors
|
||||
// if (sub.archive) {
|
||||
// archive_dir = sub.archive;
|
||||
// archive_path = path.join(archive_dir, 'archive.txt')
|
||||
// fs.appendFileSync(archive_path, output['id']);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
const outputs = err.stdout.split(/\r\n|\r|\n/);
|
||||
const files_to_download = await handleOutputJSON(outputs, sub, user_uid);
|
||||
resolve(files_to_download);
|
||||
} catch(e) {
|
||||
logger.error('Backup method failed. See error below:');
|
||||
logger.error(e);
|
||||
}
|
||||
} else {
|
||||
logger.error('Subscription check failed!');
|
||||
}
|
||||
resolve(false);
|
||||
} else if (output) {
|
||||
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) {
|
||||
await setFreshUploads(sub, user_uid);
|
||||
checkVideosForFreshUploads(sub, user_uid);
|
||||
}
|
||||
|
||||
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
|
||||
logger.verbose('No additional videos to download for ' + sub.name);
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const output_jsons = [];
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
let output_json = null;
|
||||
try {
|
||||
output_json = JSON.parse(output[i]);
|
||||
output_jsons.push(output_json);
|
||||
} catch(e) {
|
||||
output_json = null;
|
||||
}
|
||||
if (!output_json) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const files_to_download = await getFilesToDownload(sub, output_jsons);
|
||||
const base_download_options = generateOptionsForSubscriptionDownload(sub, user_uid);
|
||||
|
||||
for (let j = 0; j < files_to_download.length; j++) {
|
||||
const file_to_download = files_to_download[j];
|
||||
await downloader_api.createDownload(file_to_download['webpage_url'], sub.type || 'video', base_download_options, user_uid, sub.id, sub.name);
|
||||
}
|
||||
|
||||
const files_to_download = await handleOutputJSON(output, sub, user_uid);
|
||||
resolve(files_to_download);
|
||||
}
|
||||
});
|
||||
@@ -309,6 +266,43 @@ async function getVideosForSub(sub, user_uid = null) {
|
||||
});
|
||||
}
|
||||
|
||||
async function handleOutputJSON(output, sub, user_uid) {
|
||||
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) {
|
||||
await setFreshUploads(sub, user_uid);
|
||||
checkVideosForFreshUploads(sub, user_uid);
|
||||
}
|
||||
|
||||
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
|
||||
logger.verbose('No additional videos to download for ' + sub.name);
|
||||
return [];
|
||||
}
|
||||
|
||||
const output_jsons = [];
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
let output_json = null;
|
||||
try {
|
||||
output_json = JSON.parse(output[i]);
|
||||
output_jsons.push(output_json);
|
||||
} catch(e) {
|
||||
output_json = null;
|
||||
}
|
||||
if (!output_json) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const files_to_download = await getFilesToDownload(sub, output_jsons);
|
||||
const base_download_options = generateOptionsForSubscriptionDownload(sub, user_uid);
|
||||
|
||||
for (let j = 0; j < files_to_download.length; j++) {
|
||||
const file_to_download = files_to_download[j];
|
||||
file_to_download['formats'] = utils.stripPropertiesFromObject(file_to_download['formats'], ['format_id', 'filesize', 'filesize_approx']); // prevent download object from blowing up in size
|
||||
await downloader_api.createDownload(file_to_download['webpage_url'], sub.type || 'video', base_download_options, user_uid, sub.id, sub.name, file_to_download);
|
||||
}
|
||||
|
||||
return files_to_download;
|
||||
}
|
||||
|
||||
function generateOptionsForSubscriptionDownload(sub, user_uid) {
|
||||
let basePath = null;
|
||||
if (user_uid)
|
||||
@@ -319,10 +313,10 @@ function generateOptionsForSubscriptionDownload(sub, user_uid) {
|
||||
let default_output = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
|
||||
|
||||
const base_download_options = {
|
||||
selectedHeight: sub.maxQuality && sub.maxQuality !== 'best' ? sub.maxQuality : null,
|
||||
maxHeight: sub.maxQuality && sub.maxQuality !== 'best' ? sub.maxQuality : null,
|
||||
customFileFolderPath: getAppendedBasePath(sub, basePath),
|
||||
customOutput: sub.custom_output ? `${sub.custom_output}` : `${default_output}`,
|
||||
customArchivePath: path.join(__dirname, basePath, 'archives', sub.name),
|
||||
customArchivePath: path.join(basePath, 'archives', sub.name),
|
||||
additionalArgs: sub.custom_args
|
||||
}
|
||||
|
||||
@@ -389,11 +383,6 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
||||
downloadConfig.push('--download-archive', archive_path);
|
||||
}
|
||||
|
||||
// if streaming only mode, just get the list of videos
|
||||
if (sub.streamingOnly) {
|
||||
downloadConfig = ['-f', 'best', '--dump-json'];
|
||||
}
|
||||
|
||||
if (sub.timerange && !redownload) {
|
||||
downloadConfig.push('--dateafter', sub.timerange);
|
||||
}
|
||||
@@ -418,9 +407,11 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
||||
|
||||
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
|
||||
if (default_downloader === 'yt-dlp') {
|
||||
downloadConfig.push('--no-clean-infojson');
|
||||
downloadConfig.push('--no-clean-info-json');
|
||||
}
|
||||
|
||||
downloadConfig = utils.filterArgs(downloadConfig, ['--write-comments']);
|
||||
|
||||
return downloadConfig;
|
||||
}
|
||||
|
||||
@@ -467,7 +458,7 @@ async function updateSubscription(sub) {
|
||||
|
||||
async function updateSubscriptionPropertyMultiple(subs, assignment_obj) {
|
||||
subs.forEach(async sub => {
|
||||
await updateSubscriptionProperty(sub, assignment_obj, sub.user_uid);
|
||||
await updateSubscriptionProperty(sub, assignment_obj);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -479,6 +470,7 @@ async function updateSubscriptionProperty(sub, assignment_obj) {
|
||||
|
||||
async function setFreshUploads(sub) {
|
||||
const sub_files = await db_api.getRecords('files', {sub_id: sub.id});
|
||||
if (!sub_files) return;
|
||||
const current_date = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
||||
sub_files.forEach(async file => {
|
||||
if (current_date === file['upload_date'].replace(/-/g, '')) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
var assert = require('assert');
|
||||
const assert = require('assert');
|
||||
const low = require('lowdb')
|
||||
var winston = require('winston');
|
||||
const winston = require('winston');
|
||||
const path = require('path');
|
||||
|
||||
process.chdir('./backend')
|
||||
|
||||
@@ -39,6 +40,7 @@ const utils = require('../utils');
|
||||
const subscriptions_api = require('../subscriptions');
|
||||
const fs = require('fs-extra');
|
||||
const { uuid } = require('uuidv4');
|
||||
const NodeID3 = require('node-id3');
|
||||
|
||||
db_api.initialize(db, users_db);
|
||||
|
||||
@@ -399,6 +401,19 @@ describe('Downloader', function() {
|
||||
|
||||
});
|
||||
|
||||
it('Tag file', async function() {
|
||||
const audio_path = './test/sample.mp3';
|
||||
const sample_json = fs.readJSONSync('./test/sample.info.json');
|
||||
const tags = {
|
||||
title: sample_json['title'],
|
||||
artist: sample_json['artist'] ? sample_json['artist'] : sample_json['uploader'],
|
||||
TRCK: '27'
|
||||
}
|
||||
NodeID3.write(tags, audio_path);
|
||||
const written_tags = NodeID3.read(audio_path);
|
||||
assert(written_tags['raw']['TRCK'] === '27');
|
||||
});
|
||||
|
||||
it('Queue file', async function() {
|
||||
this.timeout(300000);
|
||||
const returned_download = await downloader_api.createDownload(url, 'video', options);
|
||||
@@ -451,6 +466,20 @@ describe('Downloader', function() {
|
||||
console.log(updated_args2);
|
||||
assert(JSON.stringify(updated_args2), JSON.stringify(expected_args2));
|
||||
});
|
||||
describe('Twitch', async function () {
|
||||
const twitch_api = require('../twitch');
|
||||
const example_vod = '1493770675';
|
||||
it('Download VOD', async function() {
|
||||
const sample_path = path.join('test', 'sample.twitch_chat.json');
|
||||
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
|
||||
this.timeout(300000);
|
||||
await twitch_api.downloadTwitchChatByVODID(example_vod, 'sample', null, null, null, './test');
|
||||
assert(fs.existsSync(sample_path));
|
||||
|
||||
// cleanup
|
||||
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tasks', function() {
|
||||
@@ -561,4 +590,40 @@ describe('Tasks', function() {
|
||||
const dummy_task_obj = await db_api.getRecord('tasks', {key: 'dummy_task'});
|
||||
assert(dummy_task_obj['data']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Archive', async function() {
|
||||
const archive_path = path.join('test', 'archives');
|
||||
fs.ensureDirSync(archive_path);
|
||||
const archive_file_path = path.join(archive_path, 'archive_video.txt');
|
||||
const blacklist_file_path = path.join(archive_path, 'blacklist_video.txt');
|
||||
beforeEach(async function() {
|
||||
if (fs.existsSync(archive_file_path)) fs.unlinkSync(archive_file_path);
|
||||
fs.writeFileSync(archive_file_path, 'youtube testing1\nyoutube testing2\nyoutube testing3\n');
|
||||
|
||||
if (fs.existsSync(blacklist_file_path)) fs.unlinkSync(blacklist_file_path);
|
||||
fs.writeFileSync(blacklist_file_path, '');
|
||||
});
|
||||
|
||||
it('Delete from archive', async function() {
|
||||
await utils.deleteFileFromArchive('N/A', 'video', archive_path, 'testing2', false);
|
||||
const new_archive = fs.readFileSync(archive_file_path);
|
||||
assert(!new_archive.includes('testing2'));
|
||||
});
|
||||
|
||||
it('Delete from archive - blacklist', async function() {
|
||||
await utils.deleteFileFromArchive('N/A', 'video', archive_path, 'testing2', true);
|
||||
const new_archive = fs.readFileSync(archive_file_path);
|
||||
const new_blacklist = fs.readFileSync(blacklist_file_path);
|
||||
assert(!new_archive.includes('testing2'));
|
||||
assert(new_blacklist.includes('testing2'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Utils', async function() {
|
||||
it('Strip properties', async function() {
|
||||
const test_obj = {test1: 'test1', test2: 'test2', test3: 'test3'};
|
||||
const stripped_obj = utils.stripPropertiesFromObject(test_obj, ['test1', 'test3']);
|
||||
assert(!stripped_obj['test1'] && stripped_obj['test2'] && !stripped_obj['test3'])
|
||||
});
|
||||
});
|
||||
@@ -1,90 +1,64 @@
|
||||
var moment = require('moment');
|
||||
var Axios = require('axios');
|
||||
var fs = require('fs-extra')
|
||||
var path = require('path');
|
||||
const config_api = require('./config');
|
||||
const logger = require('./logger');
|
||||
|
||||
async function getCommentsForVOD(clientID, vodId) {
|
||||
let url = `https://api.twitch.tv/v5/videos/${vodId}/comments?content_offset_seconds=0`,
|
||||
batch,
|
||||
cursor;
|
||||
const moment = require('moment');
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path');
|
||||
|
||||
let comments = null;
|
||||
|
||||
try {
|
||||
do {
|
||||
batch = (await Axios.get(url, {
|
||||
headers: {
|
||||
'Client-ID': clientID,
|
||||
Accept: 'application/vnd.twitchtv.v5+json; charset=UTF-8',
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
}
|
||||
})).data;
|
||||
|
||||
const str = batch.comments.map(c => {
|
||||
let {
|
||||
created_at: msgCreated,
|
||||
content_offset_seconds: timestamp,
|
||||
commenter: {
|
||||
name,
|
||||
_id,
|
||||
created_at: acctCreated
|
||||
},
|
||||
message: {
|
||||
body: msg,
|
||||
user_color: user_color
|
||||
}
|
||||
} = c;
|
||||
|
||||
const timestamp_str = moment.duration(timestamp, 'seconds')
|
||||
.toISOString()
|
||||
.replace(/P.*?T(?:(\d+?)H)?(?:(\d+?)M)?(?:(\d+).*?S)?/,
|
||||
(_, ...ms) => {
|
||||
const seg = v => v ? v.padStart(2, '0') : '00';
|
||||
return `${seg(ms[0])}:${seg(ms[1])}:${seg(ms[2])}`;
|
||||
});
|
||||
|
||||
acctCreated = moment(acctCreated).utc();
|
||||
msgCreated = moment(msgCreated).utc();
|
||||
|
||||
if (!comments) comments = [];
|
||||
|
||||
comments.push({
|
||||
timestamp: timestamp,
|
||||
timestamp_str: timestamp_str,
|
||||
name: name,
|
||||
message: msg,
|
||||
user_color: user_color
|
||||
});
|
||||
// let line = `${timestamp},${msgCreated.format(tsFormat)},${name},${_id},"${msg.replace(/"/g, '""')}",${acctCreated.format(tsFormat)}`;
|
||||
// return line;
|
||||
}).join('\n');
|
||||
|
||||
cursor = batch._next;
|
||||
url = `https://api.twitch.tv/v5/videos/${vodId}/comments?cursor=${cursor}`;
|
||||
await new Promise(res => setTimeout(res, 300));
|
||||
} while (cursor);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
async function getCommentsForVOD(clientID, clientSecret, vodId) {
|
||||
const { promisify } = require('util');
|
||||
const child_process = require('child_process');
|
||||
const exec = promisify(child_process.exec);
|
||||
|
||||
// Reject invalid params to prevent command injection attack
|
||||
if (!clientID.match(/^[0-9a-z]+$/) || !clientSecret.match(/^[0-9a-z]+$/) || !vodId.match(/^[0-9a-z]+$/)) {
|
||||
logger.error('Client ID, client secret, and VOD ID must be purely alphanumeric. Twitch chat download failed!');
|
||||
return null;
|
||||
}
|
||||
|
||||
return comments;
|
||||
const result = await exec(`tcd --video ${vodId} --client-id ${clientID} --client-secret ${clientSecret} --format json -o appdata`, {stdio:[0,1,2]});
|
||||
|
||||
if (result['stderr']) {
|
||||
logger.error(`Failed to download twitch comments for ${vodId}`);
|
||||
logger.error(result['stderr']);
|
||||
return null;
|
||||
}
|
||||
|
||||
const temp_chat_path = path.join('appdata', `${vodId}.json`);
|
||||
|
||||
const raw_json = fs.readJSONSync(temp_chat_path);
|
||||
const new_json = raw_json.comments.map(comment_obj => {
|
||||
return {
|
||||
timestamp: comment_obj.content_offset_seconds,
|
||||
timestamp_str: convertTimestamp(comment_obj.content_offset_seconds),
|
||||
name: comment_obj.commenter.name,
|
||||
message: comment_obj.message.body,
|
||||
user_color: comment_obj.message.user_color
|
||||
}
|
||||
});
|
||||
|
||||
fs.unlinkSync(temp_chat_path);
|
||||
|
||||
return new_json;
|
||||
}
|
||||
|
||||
async function getTwitchChatByFileID(id, type, user_uid, uuid, sub) {
|
||||
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
let file_path = null;
|
||||
|
||||
if (user_uid) {
|
||||
if (sub) {
|
||||
file_path = path.join('users', user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
|
||||
file_path = path.join(usersFileFolder, user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, `${id}.twitch_chat.json`);
|
||||
} else {
|
||||
file_path = path.join('users', user_uid, type, id + '.twitch_chat.json');
|
||||
file_path = path.join(usersFileFolder, user_uid, type, `${id}.twitch_chat.json`);
|
||||
}
|
||||
} else {
|
||||
if (sub) {
|
||||
file_path = path.join('subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
|
||||
file_path = path.join(subscriptionsFileFolder, sub.isPlaylist ? 'playlists' : 'channels', sub.name, `${id}.twitch_chat.json`);
|
||||
} else {
|
||||
file_path = path.join(type, id + '.twitch_chat.json');
|
||||
const typeFolder = config_api.getConfigItem(`ytdl_${type}_folder_path`);
|
||||
file_path = path.join(typeFolder, `${id}.twitch_chat.json`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,23 +70,28 @@ async function getTwitchChatByFileID(id, type, user_uid, uuid, sub) {
|
||||
return chat_file;
|
||||
}
|
||||
|
||||
async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub) {
|
||||
const twitch_api_key = config_api.getConfigItem('ytdl_twitch_api_key');
|
||||
const chat = await getCommentsForVOD(twitch_api_key, vodId);
|
||||
async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub, customFileFolderPath = null) {
|
||||
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
const twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
|
||||
const twitch_client_secret = config_api.getConfigItem('ytdl_twitch_client_secret');
|
||||
const chat = await getCommentsForVOD(twitch_client_id, twitch_client_secret, vodId);
|
||||
|
||||
// save file if needed params are included
|
||||
let file_path = null;
|
||||
if (user_uid) {
|
||||
if (customFileFolderPath) {
|
||||
file_path = path.join(customFileFolderPath, `${id}.twitch_chat.json`)
|
||||
} else if (user_uid) {
|
||||
if (sub) {
|
||||
file_path = path.join('users', user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
|
||||
file_path = path.join(usersFileFolder, user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, `${id}.twitch_chat.json`);
|
||||
} else {
|
||||
file_path = path.join('users', user_uid, type, id + '.twitch_chat.json');
|
||||
file_path = path.join(usersFileFolder, user_uid, type, `${id}.twitch_chat.json`);
|
||||
}
|
||||
} else {
|
||||
if (sub) {
|
||||
file_path = path.join('subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
|
||||
file_path = path.join(subscriptionsFileFolder, sub.isPlaylist ? 'playlists' : 'channels', sub.name, `${id}.twitch_chat.json`);
|
||||
} else {
|
||||
file_path = path.join(type, id + '.twitch_chat.json');
|
||||
file_path = path.join(type, `${id}.twitch_chat.json`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +100,14 @@ async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub) {
|
||||
return chat;
|
||||
}
|
||||
|
||||
const convertTimestamp = (timestamp) => moment.duration(timestamp, 'seconds')
|
||||
.toISOString()
|
||||
.replace(/P.*?T(?:(\d+?)H)?(?:(\d+?)M)?(?:(\d+).*?S)?/,
|
||||
(_, ...ms) => {
|
||||
const seg = v => v ? v.padStart(2, '0') : '00';
|
||||
return `${seg(ms[0])}:${seg(ms[1])}:${seg(ms[2])}`;
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
getCommentsForVOD: getCommentsForVOD,
|
||||
getTwitchChatByFileID: getTwitchChatByFileID,
|
||||
|
||||
101
backend/utils.js
101
backend/utils.js
@@ -172,11 +172,13 @@ function getExpectedFileSize(input_info_jsons) {
|
||||
const formats = info_json['format_id'].split('+');
|
||||
let individual_expected_filesize = 0;
|
||||
formats.forEach(format_id => {
|
||||
info_json.formats.forEach(available_format => {
|
||||
if (available_format.format_id === format_id && available_format.filesize) {
|
||||
individual_expected_filesize += available_format.filesize;
|
||||
}
|
||||
});
|
||||
if (info_json.formats !== undefined) {
|
||||
info_json.formats.forEach(available_format => {
|
||||
if (available_format.format_id === format_id && (available_format.filesize || available_format.filesize_approx)) {
|
||||
individual_expected_filesize += (available_format.filesize ? available_format.filesize : available_format.filesize_approx);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
expected_filesize += individual_expected_filesize;
|
||||
});
|
||||
@@ -210,7 +212,7 @@ function deleteJSONFile(file_path, type) {
|
||||
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||
|
||||
const file_path_no_extension = removeFileExtension(file_path);
|
||||
|
||||
|
||||
let json_path = file_path_no_extension + '.info.json';
|
||||
let alternate_json_path = file_path_no_extension + ext + '.info.json';
|
||||
|
||||
@@ -218,8 +220,11 @@ function deleteJSONFile(file_path, type) {
|
||||
if (fs.existsSync(alternate_json_path)) fs.unlinkSync(alternate_json_path);
|
||||
}
|
||||
|
||||
async function removeIDFromArchive(archive_path, id) {
|
||||
let data = await fs.readFile(archive_path, {encoding: 'utf-8'});
|
||||
// archive helper functions
|
||||
|
||||
async function removeIDFromArchive(archive_path, type, id) {
|
||||
const archive_file = path.join(archive_path, `archive_${type}.txt`);
|
||||
const data = await fs.readFile(archive_file, {encoding: 'utf-8'});
|
||||
if (!data) {
|
||||
logger.error('Archive could not be found.');
|
||||
return;
|
||||
@@ -236,12 +241,34 @@ async function removeIDFromArchive(archive_path, id) {
|
||||
}
|
||||
}
|
||||
|
||||
if (lastIndex === -1) return null;
|
||||
|
||||
const line = dataArray.splice(lastIndex, 1); // remove the keyword id from the data Array
|
||||
|
||||
// UPDATE FILE WITH NEW DATA
|
||||
const updatedData = dataArray.join('\n');
|
||||
await fs.writeFile(archive_path, updatedData);
|
||||
if (line) return line;
|
||||
await fs.writeFile(archive_file, updatedData);
|
||||
if (line) return Array.isArray(line) && line.length === 1 ? line[0] : line;
|
||||
}
|
||||
|
||||
async function writeToBlacklist(archive_folder, type, line) {
|
||||
let blacklistPath = path.join(archive_folder, (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);
|
||||
}
|
||||
|
||||
async function deleteFileFromArchive(uid, type, archive_path, id, blacklistMode) {
|
||||
const archive_file = path.join(archive_path, `archive_${type}.txt`);
|
||||
if (await fs.pathExists(archive_path)) {
|
||||
const line = id ? await removeIDFromArchive(archive_path, type, id) : null;
|
||||
if (blacklistMode && line) await writeToBlacklist(archive_path, type, line);
|
||||
} else {
|
||||
logger.info(`Could not find archive file for file ${uid}. Creating...`);
|
||||
await fs.close(await fs.open(archive_file, 'w'));
|
||||
}
|
||||
}
|
||||
|
||||
function durationStringToNumber(dur_str) {
|
||||
@@ -306,9 +333,9 @@ function createEdgeNGrams(str) {
|
||||
if (str && str.length > 3) {
|
||||
const minGram = 3
|
||||
const maxGram = str.length
|
||||
|
||||
|
||||
return str.split(" ").reduce((ngrams, token) => {
|
||||
if (token.length > minGram) {
|
||||
if (token.length > minGram) {
|
||||
for (let i = minGram; i <= maxGram && i <= token.length; ++i) {
|
||||
ngrams = [...ngrams, token.substr(0, i)]
|
||||
}
|
||||
@@ -318,7 +345,7 @@ function createEdgeNGrams(str) {
|
||||
return ngrams
|
||||
}, []).join(" ")
|
||||
}
|
||||
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
@@ -418,7 +445,7 @@ async function fetchFile(url, path, file_label) {
|
||||
async function restartServer(is_update = false) {
|
||||
logger.info(`${is_update ? 'Update complete! ' : ''}Restarting server...`);
|
||||
|
||||
// the following line restarts the server through nodemon
|
||||
// the following line restarts the server through pm2
|
||||
fs.writeFileSync(`restart${is_update ? '_update' : '_general'}.json`, 'internal use only');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -434,7 +461,7 @@ function injectArgs(original_args, new_args) {
|
||||
for (let i = 0; i < new_args.length; i++) {
|
||||
const new_arg = new_args[i];
|
||||
if (!new_arg.startsWith('-') && !new_arg.startsWith('--') && i > 0 && original_args.includes(new_args[i - 1])) continue;
|
||||
|
||||
|
||||
if (CONSTS.YTDL_ARGS_WITH_VALUES.has(new_arg)) {
|
||||
if (original_args.includes(new_arg)) {
|
||||
const original_index = original_args.indexOf(new_arg);
|
||||
@@ -456,6 +483,10 @@ function injectArgs(original_args, new_args) {
|
||||
return updated_args;
|
||||
}
|
||||
|
||||
function filterArgs(args, args_to_remove) {
|
||||
return args.filter(x => !args_to_remove.includes(x));
|
||||
}
|
||||
|
||||
const searchObjectByString = function(o, s) {
|
||||
s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
|
||||
s = s.replace(/^\./, ''); // strip a leading dot
|
||||
@@ -471,6 +502,41 @@ const searchObjectByString = function(o, s) {
|
||||
return o;
|
||||
}
|
||||
|
||||
function stripPropertiesFromObject(obj, properties, whitelist = false) {
|
||||
if (!whitelist) {
|
||||
const new_obj = JSON.parse(JSON.stringify(obj));
|
||||
for (let field of properties) {
|
||||
delete new_obj[field];
|
||||
}
|
||||
return new_obj;
|
||||
}
|
||||
|
||||
const new_obj = {};
|
||||
for (let field of properties) {
|
||||
new_obj[field] = obj[field];
|
||||
}
|
||||
return new_obj;
|
||||
}
|
||||
|
||||
function getArchiveFolder(type, user_uid = null, sub = null) {
|
||||
const usersFolderPath = config_api.getConfigItem('ytdl_users_base_path');
|
||||
const subsFolderPath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
|
||||
if (user_uid) {
|
||||
if (sub) {
|
||||
return path.join(usersFolderPath, user_uid, 'subscriptions', 'archives', sub.name);
|
||||
} else {
|
||||
return path.join(usersFolderPath, user_uid, type, 'archives');
|
||||
}
|
||||
} else {
|
||||
if (sub) {
|
||||
return path.join(subsFolderPath, 'archives', sub.name);
|
||||
} else {
|
||||
return path.join('appdata', 'archives');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// objects
|
||||
|
||||
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {
|
||||
@@ -500,6 +566,8 @@ module.exports = {
|
||||
fixVideoMetadataPerms: fixVideoMetadataPerms,
|
||||
deleteJSONFile: deleteJSONFile,
|
||||
removeIDFromArchive: removeIDFromArchive,
|
||||
writeToBlacklist: writeToBlacklist,
|
||||
deleteFileFromArchive: deleteFileFromArchive,
|
||||
getDownloadedFilesByType: getDownloadedFilesByType,
|
||||
createContainerZipFile: createContainerZipFile,
|
||||
durationStringToNumber: durationStringToNumber,
|
||||
@@ -515,6 +583,9 @@ module.exports = {
|
||||
fetchFile: fetchFile,
|
||||
restartServer: restartServer,
|
||||
injectArgs: injectArgs,
|
||||
filterArgs: filterArgs,
|
||||
searchObjectByString: searchObjectByString,
|
||||
stripPropertiesFromObject: stripPropertiesFromObject,
|
||||
getArchiveFolder: getArchiveFolder,
|
||||
File: File
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ exports.updateYoutubeDL = async (latest_update_version) => {
|
||||
|
||||
exports.verifyBinaryExistsLinux = () => {
|
||||
const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH);
|
||||
if (!is_windows && details_json && (details_json['path'].includes('.exe') || !details_json['path'])) {
|
||||
if (!is_windows && details_json && (!details_json['path'] || details_json['path'].includes('.exe'))) {
|
||||
details_json['path'] = 'node_modules/youtube-dl/bin/youtube-dl';
|
||||
details_json['exec'] = 'youtube-dl';
|
||||
details_json['version'] = OUTDATED_VERSION;
|
||||
|
||||
@@ -21,4 +21,4 @@ version: 0.1.0
|
||||
# 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"
|
||||
appVersion: "4.3"
|
||||
|
||||
@@ -2,7 +2,6 @@ version: "2"
|
||||
services:
|
||||
ytdl_material:
|
||||
environment:
|
||||
ALLOW_CONFIG_MUTATIONS: 'true'
|
||||
ytdl_mongodb_connection_string: 'mongodb://ytdl-mongo-db:27017'
|
||||
ytdl_use_local_db: 'false'
|
||||
write_ytdl_config: 'true'
|
||||
@@ -17,14 +16,12 @@ services:
|
||||
- ./users:/app/users
|
||||
ports:
|
||||
- "8998:17442"
|
||||
image: tzahi12345/youtubedl-material:nightly
|
||||
image: tzahi12345/youtubedl-material:latest
|
||||
ytdl-mongo-db:
|
||||
image: mongo
|
||||
ports:
|
||||
- "27017:27017"
|
||||
logging:
|
||||
driver: "none"
|
||||
container_name: mongo-db
|
||||
restart: always
|
||||
volumes:
|
||||
- ./db/:/data/db
|
||||
- ./db/:/data/db
|
||||
|
||||
3740
package-lock.json
generated
3740
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtube-dl-material",
|
||||
"version": "4.2.0",
|
||||
"version": "4.3.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
@@ -13,7 +13,7 @@
|
||||
"e2e": "ng e2e",
|
||||
"electron": "ng build --base-href ./ && electron .",
|
||||
"generate": "openapi --input ./\"Public API v1.yaml\" --output ./src/api-types --exportCore false --exportServices false --exportModels true",
|
||||
"i18n-source": "ng extract-i18n --output-path=src/assets/i18n"
|
||||
"i18n-source": "ng extract-i18n --output-path=src/assets/i18n --out-file=messages.en.xlf"
|
||||
},
|
||||
"engines": {
|
||||
"node": "12.3.1",
|
||||
@@ -21,18 +21,18 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "^13.3.3",
|
||||
"@angular/animations": "^13.3.4",
|
||||
"@angular/cdk": "^13.3.4",
|
||||
"@angular/common": "^13.3.4",
|
||||
"@angular/compiler": "^13.3.4",
|
||||
"@angular/core": "^13.3.4",
|
||||
"@angular/forms": "^13.3.4",
|
||||
"@angular/localize": "^13.3.4",
|
||||
"@angular/material": "^13.3.4",
|
||||
"@angular/platform-browser": "^13.3.4",
|
||||
"@angular/platform-browser-dynamic": "^13.3.4",
|
||||
"@angular/router": "^13.3.4",
|
||||
"@angular-devkit/core": "^14.0.4",
|
||||
"@angular/animations": "^14.0.4",
|
||||
"@angular/cdk": "^14.0.4",
|
||||
"@angular/common": "^14.0.4",
|
||||
"@angular/compiler": "^14.0.4",
|
||||
"@angular/core": "^14.0.4",
|
||||
"@angular/forms": "^14.0.4",
|
||||
"@angular/localize": "^14.0.4",
|
||||
"@angular/material": "^14.0.4",
|
||||
"@angular/platform-browser": "^14.0.4",
|
||||
"@angular/platform-browser-dynamic": "^14.0.4",
|
||||
"@angular/router": "^14.0.4",
|
||||
"@fontsource/material-icons": "^4.5.4",
|
||||
"@ngneat/content-loader": "^5.0.0",
|
||||
"@videogular/ngx-videogular": "^5.0.1",
|
||||
@@ -55,10 +55,10 @@
|
||||
"zone.js": "~0.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^13.3.3",
|
||||
"@angular/cli": "^13.3.3",
|
||||
"@angular/compiler-cli": "^13.3.4",
|
||||
"@angular/language-service": "^13.3.4",
|
||||
"@angular-devkit/build-angular": "^14.0.4",
|
||||
"@angular/cli": "^14.0.4",
|
||||
"@angular/compiler-cli": "^14.0.4",
|
||||
"@angular/language-service": "^14.0.4",
|
||||
"@types/core-js": "^2.5.2",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
@@ -66,7 +66,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
||||
"@typescript-eslint/parser": "^4.29.0",
|
||||
"codelyzer": "^6.0.0",
|
||||
"electron": "^13.6.6",
|
||||
"electron": "^19.0.6",
|
||||
"eslint": "^7.32.0",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
|
||||
Binary file not shown.
@@ -40,11 +40,13 @@ export type { DownloadTwitchChatByVODIDRequest } from './models/DownloadTwitchCh
|
||||
export type { DownloadTwitchChatByVODIDResponse } from './models/DownloadTwitchChatByVODIDResponse';
|
||||
export type { DownloadVideosForSubscriptionRequest } from './models/DownloadVideosForSubscriptionRequest';
|
||||
export { FileType } from './models/FileType';
|
||||
export { FileTypeFilter } from './models/FileTypeFilter';
|
||||
export type { GenerateArgsResponse } from './models/GenerateArgsResponse';
|
||||
export type { GenerateNewApiKeyResponse } from './models/GenerateNewApiKeyResponse';
|
||||
export type { GetAllCategoriesResponse } from './models/GetAllCategoriesResponse';
|
||||
export type { GetAllDownloadsRequest } from './models/GetAllDownloadsRequest';
|
||||
export type { GetAllDownloadsResponse } from './models/GetAllDownloadsResponse';
|
||||
export type { GetAllFilesRequest } from './models/GetAllFilesRequest';
|
||||
export type { GetAllFilesResponse } from './models/GetAllFilesResponse';
|
||||
export type { GetAllSubscriptionsResponse } from './models/GetAllSubscriptionsResponse';
|
||||
export type { GetAllTasksResponse } from './models/GetAllTasksResponse';
|
||||
@@ -82,6 +84,7 @@ export type { RestoreDBBackupRequest } from './models/RestoreDBBackupRequest';
|
||||
export { Schedule } from './models/Schedule';
|
||||
export type { SetConfigRequest } from './models/SetConfigRequest';
|
||||
export type { SharingToggle } from './models/SharingToggle';
|
||||
export type { Sort } from './models/Sort';
|
||||
export type { SubscribeRequest } from './models/SubscribeRequest';
|
||||
export type { SubscribeResponse } from './models/SubscribeResponse';
|
||||
export type { Subscription } from './models/Subscription';
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
import type { FileType } from './FileType';
|
||||
|
||||
export type CreatePlaylistRequest = {
|
||||
playlistName: string;
|
||||
uids: Array<string>;
|
||||
type: FileType;
|
||||
thumbnailURL: string;
|
||||
};
|
||||
@@ -30,4 +30,14 @@ export type DatabaseFile = {
|
||||
category?: Category;
|
||||
view_count?: number;
|
||||
local_view_count?: number;
|
||||
sub_id?: string;
|
||||
registered?: number;
|
||||
/**
|
||||
* In pixels, only for videos
|
||||
*/
|
||||
height?: number;
|
||||
/**
|
||||
* In Kbps
|
||||
*/
|
||||
abr?: number;
|
||||
};
|
||||
@@ -2,9 +2,6 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
import type { FileType } from './FileType';
|
||||
|
||||
export type DeletePlaylistRequest = {
|
||||
playlist_id: string;
|
||||
type: FileType;
|
||||
};
|
||||
@@ -22,4 +22,5 @@ export type Download = {
|
||||
user_uid?: string;
|
||||
sub_id?: string;
|
||||
sub_name?: string;
|
||||
prefetched_info?: any;
|
||||
};
|
||||
9
src/api-types/models/FileTypeFilter.ts
Normal file
9
src/api-types/models/FileTypeFilter.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
export enum FileTypeFilter {
|
||||
AUDIO_ONLY = 'audio_only',
|
||||
VIDEO_ONLY = 'video_only',
|
||||
BOTH = 'both',
|
||||
}
|
||||
20
src/api-types/models/GetAllFilesRequest.ts
Normal file
20
src/api-types/models/GetAllFilesRequest.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
import type { FileTypeFilter } from './FileTypeFilter';
|
||||
import type { Sort } from './Sort';
|
||||
|
||||
export type GetAllFilesRequest = {
|
||||
sort?: Sort;
|
||||
range?: Array<number>;
|
||||
/**
|
||||
* Filter files by title
|
||||
*/
|
||||
text_search?: string;
|
||||
file_type_filter?: FileTypeFilter;
|
||||
/**
|
||||
* Include if you want to filter by subscription
|
||||
*/
|
||||
sub_id?: string;
|
||||
};
|
||||
@@ -2,11 +2,14 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
import type { FileType } from './FileType';
|
||||
import type { DatabaseFile } from './DatabaseFile';
|
||||
import type { Playlist } from './Playlist';
|
||||
|
||||
export type GetPlaylistResponse = {
|
||||
playlist: Playlist;
|
||||
type: FileType;
|
||||
success: boolean;
|
||||
/**
|
||||
* File objects for every uid in the playlist's uids property, in the same order
|
||||
*/
|
||||
file_objs?: Array<DatabaseFile>;
|
||||
};
|
||||
14
src/api-types/models/Sort.ts
Normal file
14
src/api-types/models/Sort.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
export type Sort = {
|
||||
/**
|
||||
* Property to sort by
|
||||
*/
|
||||
by?: string;
|
||||
/**
|
||||
* 1 for ascending, -1 for descending
|
||||
*/
|
||||
order?: number;
|
||||
};
|
||||
@@ -10,7 +10,6 @@ export type Subscription = {
|
||||
id: string;
|
||||
type: FileType;
|
||||
user_uid: string | null;
|
||||
streamingOnly: boolean;
|
||||
isPlaylist: boolean;
|
||||
archive?: string;
|
||||
timerange?: string;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
export type Task = {
|
||||
key: string;
|
||||
title?: string;
|
||||
last_ran: number;
|
||||
last_confirmed: number;
|
||||
running: boolean;
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<a *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode && !postsService.isLoggedIn" mat-list-item (click)="sidenav.close()" routerLink='/login'><ng-container i18n="Navigation menu Login Page title">Login</ng-container></a>
|
||||
<a *ngIf="postsService.config && allowSubscriptions && postsService.hasPermission('subscriptions')" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
|
||||
<a *ngIf="postsService.config && enableDownloadsManager && postsService.hasPermission('downloads_manager')" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
|
||||
<a *ngIf="postsService.config && enableDownloadsManager && postsService.hasPermission('downloads_manager')" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/tasks'><ng-container i18n="Navigation menu Tasks Page title">Tasks</ng-container></a>
|
||||
<a *ngIf="postsService.config && postsService.hasPermission('tasks_manager')" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/tasks'><ng-container i18n="Navigation menu Tasks Page title">Tasks</ng-container></a>
|
||||
<ng-container *ngIf="postsService.config && postsService.hasPermission('settings')">
|
||||
<mat-divider></mat-divider>
|
||||
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/settings'><ng-container i18n="Settings menu label">Settings</ng-container></a>
|
||||
|
||||
@@ -49,7 +49,6 @@ import { CreatePlaylistComponent } from './create-playlist/create-playlist.compo
|
||||
import { SubscriptionsComponent } from './subscriptions/subscriptions.component';
|
||||
import { SubscribeDialogComponent } from './dialogs/subscribe-dialog/subscribe-dialog.component';
|
||||
import { SubscriptionComponent } from './subscription//subscription/subscription.component';
|
||||
import { SubscriptionFileCardComponent } from './subscription/subscription-file-card/subscription-file-card.component';
|
||||
import { SubscriptionInfoDialogComponent } from './dialogs/subscription-info-dialog/subscription-info-dialog.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
@@ -74,7 +73,6 @@ import { ManageUserComponent } from './components/manage-user/manage-user.compon
|
||||
import { ManageRoleComponent } from './components/manage-role/manage-role.component';
|
||||
import { CookiesUploaderDialogComponent } from './dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component';
|
||||
import { LogsViewerComponent } from './components/logs-viewer/logs-viewer.component';
|
||||
import { ModifyPlaylistComponent } from './dialogs/modify-playlist/modify-playlist.component';
|
||||
import { ConfirmDialogComponent } from './dialogs/confirm-dialog/confirm-dialog.component';
|
||||
import { UnifiedFileCardComponent } from './components/unified-file-card/unified-file-card.component';
|
||||
import { RecentVideosComponent } from './components/recent-videos/recent-videos.component';
|
||||
@@ -102,7 +100,6 @@ registerLocaleData(es, 'es');
|
||||
SubscriptionsComponent,
|
||||
SubscribeDialogComponent,
|
||||
SubscriptionComponent,
|
||||
SubscriptionFileCardComponent,
|
||||
SubscriptionInfoDialogComponent,
|
||||
SettingsComponent,
|
||||
AboutDialogComponent,
|
||||
@@ -123,7 +120,6 @@ registerLocaleData(es, 'es');
|
||||
ManageRoleComponent,
|
||||
CookiesUploaderDialogComponent,
|
||||
LogsViewerComponent,
|
||||
ModifyPlaylistComponent,
|
||||
ConfirmDialogComponent,
|
||||
UnifiedFileCardComponent,
|
||||
RecentVideosComponent,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { PostsService } from 'app/posts.services';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.component';
|
||||
import { ModifyPlaylistComponent } from 'app/dialogs/modify-playlist/modify-playlist.component';
|
||||
import { Playlist } from 'api-types';
|
||||
|
||||
@Component({
|
||||
selector: 'app-custom-playlists',
|
||||
@@ -32,7 +32,7 @@ export class CustomPlaylistsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
getAllPlaylists() {
|
||||
getAllPlaylists(): void {
|
||||
this.playlists_received = false;
|
||||
// must call getAllFiles as we need to get category playlists as well
|
||||
this.postsService.getPlaylists(true).subscribe(res => {
|
||||
@@ -42,22 +42,25 @@ export class CustomPlaylistsComponent implements OnInit {
|
||||
}
|
||||
|
||||
// creating a playlist
|
||||
openCreatePlaylistDialog() {
|
||||
openCreatePlaylistDialog(): void {
|
||||
const dialogRef = this.dialog.open(CreatePlaylistComponent, {
|
||||
data: {
|
||||
}
|
||||
create_mode: true
|
||||
},
|
||||
minWidth: '90vw',
|
||||
minHeight: '95vh'
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.getAllPlaylists();
|
||||
this.postsService.openSnackBar('Successfully created playlist!', '');
|
||||
this.postsService.openSnackBar($localize`Successfully created playlist!`);
|
||||
} else if (result === false) {
|
||||
this.postsService.openSnackBar('ERROR: failed to create playlist!', '');
|
||||
this.postsService.openSnackBar($localize`ERROR: failed to create playlist!`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
goToPlaylist(info_obj) {
|
||||
goToPlaylist(info_obj: { file: Playlist; }): void {
|
||||
const playlist = info_obj.file;
|
||||
const playlistID = playlist.id;
|
||||
|
||||
@@ -72,11 +75,12 @@ export class CustomPlaylistsComponent implements OnInit {
|
||||
}
|
||||
} else {
|
||||
// playlist not found
|
||||
// TODO: Make translatable
|
||||
console.error(`Playlist with ID ${playlistID} not found!`);
|
||||
}
|
||||
}
|
||||
|
||||
downloadPlaylist(playlist_id, playlist_name) {
|
||||
downloadPlaylist(playlist_id: string, playlist_name: string): void {
|
||||
this.downloading_content[playlist_id] = true;
|
||||
this.postsService.downloadPlaylistFromServer(playlist_id).subscribe(res => {
|
||||
this.downloading_content[playlist_id] = false;
|
||||
@@ -86,33 +90,34 @@ export class CustomPlaylistsComponent implements OnInit {
|
||||
|
||||
}
|
||||
|
||||
deletePlaylist(args) {
|
||||
deletePlaylist(args: { file: Playlist; index: number; }): void {
|
||||
const playlist = args.file;
|
||||
const index = args.index;
|
||||
const playlistID = playlist.id;
|
||||
this.postsService.removePlaylist(playlistID, playlist.type).subscribe(res => {
|
||||
this.postsService.removePlaylist(playlistID).subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.playlists.splice(index, 1);
|
||||
this.postsService.openSnackBar('Playlist successfully removed.', '');
|
||||
this.postsService.openSnackBar($localize`Playlist successfully removed.`);
|
||||
}
|
||||
this.getAllPlaylists();
|
||||
});
|
||||
}
|
||||
|
||||
editPlaylistDialog(args) {
|
||||
editPlaylistDialog(args: { playlist: Playlist; index: number; }): void {
|
||||
const playlist = args.playlist;
|
||||
const index = args.index;
|
||||
const dialogRef = this.dialog.open(ModifyPlaylistComponent, {
|
||||
const dialogRef = this.dialog.open(CreatePlaylistComponent, {
|
||||
data: {
|
||||
playlist_id: playlist.id,
|
||||
width: '65vw'
|
||||
}
|
||||
create_mode: false
|
||||
},
|
||||
minWidth: '85vw'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(res => {
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
// updates playlist in file manager if it changed
|
||||
if (dialogRef.componentInstance.playlist_updated) {
|
||||
this.playlists[index] = dialogRef.componentInstance.original_playlist;
|
||||
this.playlists[index] = dialogRef.componentInstance.playlist;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { MatDialog } from '@angular/material/dialog';
|
||||
import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialog.component';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { Clipboard } from '@angular/cdk/clipboard';
|
||||
import { Download } from 'api-types';
|
||||
|
||||
@Component({
|
||||
selector: 'app-downloads',
|
||||
@@ -68,7 +69,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
||||
sort_downloads = (a, b) => {
|
||||
sort_downloads = (a: Download, b: Download): number => {
|
||||
const result = b.timestamp_start - a.timestamp_start;
|
||||
return result;
|
||||
}
|
||||
@@ -166,7 +167,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
||||
pauseDownload(download_uid: string): void {
|
||||
this.postsService.pauseDownload(download_uid).subscribe(res => {
|
||||
if (!res['success']) {
|
||||
this.postsService.openSnackBar('Failed to pause download! See server logs for more info.');
|
||||
this.postsService.openSnackBar($localize`Failed to pause download! See server logs for more info.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -174,7 +175,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
||||
pauseAllDownloads(): void {
|
||||
this.postsService.pauseAllDownloads().subscribe(res => {
|
||||
if (!res['success']) {
|
||||
this.postsService.openSnackBar('Failed to pause all downloads! See server logs for more info.');
|
||||
this.postsService.openSnackBar($localize`Failed to pause all downloads! See server logs for more info.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -182,7 +183,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
||||
resumeDownload(download_uid: string): void {
|
||||
this.postsService.resumeDownload(download_uid).subscribe(res => {
|
||||
if (!res['success']) {
|
||||
this.postsService.openSnackBar('Failed to resume download! See server logs for more info.');
|
||||
this.postsService.openSnackBar($localize`Failed to resume download! See server logs for more info.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -190,7 +191,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
||||
resumeAllDownloads(): void {
|
||||
this.postsService.resumeAllDownloads().subscribe(res => {
|
||||
if (!res['success']) {
|
||||
this.postsService.openSnackBar('Failed to resume all downloads! See server logs for more info.');
|
||||
this.postsService.openSnackBar($localize`Failed to resume all downloads! See server logs for more info.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -198,7 +199,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
||||
restartDownload(download_uid: string): void {
|
||||
this.postsService.restartDownload(download_uid).subscribe(res => {
|
||||
if (!res['success']) {
|
||||
this.postsService.openSnackBar('Failed to restart download! See server logs for more info.');
|
||||
this.postsService.openSnackBar($localize`Failed to restart download! See server logs for more info.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -206,7 +207,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
||||
cancelDownload(download_uid: string): void {
|
||||
this.postsService.cancelDownload(download_uid).subscribe(res => {
|
||||
if (!res['success']) {
|
||||
this.postsService.openSnackBar('Failed to cancel download! See server logs for more info.');
|
||||
this.postsService.openSnackBar($localize`Failed to cancel download! See server logs for more info.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -214,12 +215,12 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
||||
clearDownload(download_uid: string): void {
|
||||
this.postsService.clearDownload(download_uid).subscribe(res => {
|
||||
if (!res['success']) {
|
||||
this.postsService.openSnackBar('Failed to pause download! See server logs for more info.');
|
||||
this.postsService.openSnackBar($localize`Failed to pause download! See server logs for more info.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
watchContent(download): void {
|
||||
watchContent(download: Download): void {
|
||||
const container = download['container'];
|
||||
localStorage.setItem('player_navigator', this.router.url.split(';')[0]);
|
||||
const is_playlist = container['uids']; // hacky, TODO: fix
|
||||
@@ -230,7 +231,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
combineDownloads(downloads_old, downloads_new) {
|
||||
combineDownloads(downloads_old: Download[], downloads_new: Download[]): Download[] {
|
||||
// only keeps downloads that exist in the new set
|
||||
downloads_old = downloads_old.filter(download_old => downloads_new.some(download_new => download_new.uid === download_old.uid));
|
||||
|
||||
@@ -251,7 +252,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
||||
return downloads_old;
|
||||
}
|
||||
|
||||
showError(download) {
|
||||
showError(download: Download): void {
|
||||
const copyToClipboardEmitter = new EventEmitter<boolean>();
|
||||
this.dialog.open(ConfirmDialogComponent, {
|
||||
data: {
|
||||
@@ -272,10 +273,3 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface Download {
|
||||
timestamp_start: number;
|
||||
title: string;
|
||||
step_index: number;
|
||||
progress: string;
|
||||
}
|
||||
@@ -43,17 +43,17 @@ export class LogsViewerComponent implements OnInit {
|
||||
})
|
||||
});
|
||||
} else {
|
||||
this.postsService.openSnackBar('Failed to retrieve logs!');
|
||||
this.postsService.openSnackBar($localize`Failed to retrieve logs!`);
|
||||
}
|
||||
}, err => {
|
||||
this.logs_loading = false;
|
||||
console.error(err);
|
||||
this.postsService.openSnackBar('Failed to retrieve logs!');
|
||||
this.postsService.openSnackBar($localize`Failed to retrieve logs!`);
|
||||
});
|
||||
}
|
||||
|
||||
copiedLogsToClipboard() {
|
||||
this.postsService.openSnackBar('Logs copied to clipboard!');
|
||||
this.postsService.openSnackBar($localize`Logs copied to clipboard!`);
|
||||
}
|
||||
|
||||
clearLogs() {
|
||||
@@ -72,12 +72,12 @@ export class LogsViewerComponent implements OnInit {
|
||||
this.logs = [];
|
||||
this.logs_text = '';
|
||||
this.getLogs();
|
||||
this.postsService.openSnackBar('Logs successfully cleared!');
|
||||
this.postsService.openSnackBar($localize`Logs successfully cleared!`);
|
||||
} else {
|
||||
this.postsService.openSnackBar('Failed to clear logs!');
|
||||
this.postsService.openSnackBar($localize`Failed to clear logs!`);
|
||||
}
|
||||
}, err => {
|
||||
this.postsService.openSnackBar('Failed to clear logs!');
|
||||
this.postsService.openSnackBar($localize`Failed to clear logs!`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,12 +14,13 @@ export class ManageRoleComponent implements OnInit {
|
||||
permissions = null;
|
||||
|
||||
permissionToLabel = {
|
||||
'filemanager': 'File manager',
|
||||
'settings': 'Settings access',
|
||||
'subscriptions': 'Subscriptions',
|
||||
'sharing': 'Share files',
|
||||
'advanced_download': 'Use advanced download mode',
|
||||
'downloads_manager': 'Use downloads manager'
|
||||
'filemanager': $localize`File manager`,
|
||||
'settings': $localize`Settings access`,
|
||||
'subscriptions': $localize`Subscriptions`,
|
||||
'sharing': $localize`Share files`,
|
||||
'advanced_download': $localize`Use advanced download mode`,
|
||||
'downloads_manager': $localize`Use downloads manager`,
|
||||
'tasks_manager': $localize`Use tasks manager`,
|
||||
}
|
||||
|
||||
constructor(public postsService: PostsService, private dialogRef: MatDialogRef<ManageRoleComponent>,
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 order-1 col-sm-4 order-sm-2 d-flex justify-content-center">
|
||||
<h4 class="my-videos-title" i18n="My videos title">My videos</h4>
|
||||
<h4 *ngIf="!customHeader" class="my-videos-title" i18n="My files title">My files</h4>
|
||||
<h4 *ngIf="customHeader" class="my-videos-title">{{customHeader}}</h4>
|
||||
</div>
|
||||
<div class="col-12 order-3 col-sm-4 order-sm-3 d-flex justify-content-center">
|
||||
<mat-form-field [ngClass]="searchIsFocused ? 'search-bar-focused' : 'search-bar-unfocused'" class="search-bar" color="accent">
|
||||
@@ -28,14 +29,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="container" style="margin-bottom: 16px">
|
||||
<div *ngIf="!selectMode" class="container" style="margin-bottom: 16px">
|
||||
<div class="row justify-content-center">
|
||||
<ng-container *ngIf="normal_files_received && paged_data">
|
||||
<div *ngFor="let file of paged_data; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
||||
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [availablePlaylists]="playlists" (addFileToPlaylist)="addFileToPlaylist($event)" [loading]="false" (deleteFile)="deleteFile($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? this.postsService.token : ''"></app-unified-file-card>
|
||||
<div style="display: flex; align-items: center;" *ngFor="let file of paged_data; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
||||
<app-unified-file-card [ngClass]="downloading_content[file.uid] ? 'blurred' : ''" [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [availablePlaylists]="playlists" (addFileToPlaylist)="addFileToPlaylist($event)" [loading]="false" (deleteFile)="deleteFile($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? this.postsService.token : ''"></app-unified-file-card>
|
||||
<mat-spinner *ngIf="downloading_content[file.uid]" class="downloading-spinner" [diameter]="32"></mat-spinner>
|
||||
</div>
|
||||
<div *ngIf="paged_data.length === 0">
|
||||
<ng-container i18n="No videos found">No videos found.</ng-container>
|
||||
<ng-container i18n="No files found">No files found.</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
|
||||
@@ -46,7 +48,54 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div *ngIf="selectMode">
|
||||
<mat-tab-group [(selectedIndex)]="selectedIndex">
|
||||
<mat-tab label="Order" i18n-label="Order">
|
||||
<div *ngIf="selected_data.length">
|
||||
<span *ngIf="reverse_order === false" i18n="Normal order">Normal order </span>
|
||||
<span *ngIf="reverse_order === true" i18n="Reverse order">Reverse order </span>
|
||||
<button (click)="toggleSelectionOrder()" mat-icon-button><mat-icon>{{!reverse_order ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
|
||||
</div>
|
||||
|
||||
<!-- Selection order -->
|
||||
<mat-button-toggle-group *ngIf="selected_data.length" class="media-list" cdkDropList (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical #group="matButtonToggleGroup">
|
||||
<!-- The following for loop can be optimized but it requires a pipe https://stackoverflow.com/a/35703364/8088021 -->
|
||||
<mat-button-toggle class="media-box" cdkDrag *ngFor="let file of (reverse_order ? selected_data_objs.slice().reverse() : selected_data_objs); let i = index" [checked]="false"><div><div class="playlist-item-text">{{file.title}}</div> <button (click)="removeSelectedFile(i)" class="remove-item-button" mat-icon-button><mat-icon>cancel</mat-icon></button></div></mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
|
||||
<div style="margin-top: 20px;" *ngIf="!selected_data.length">
|
||||
<h4 style="text-align: center;">No files selected!</h4>
|
||||
</div>
|
||||
|
||||
</mat-tab>
|
||||
<mat-tab label="Select files" i18n-label="Select files">
|
||||
<mat-selection-list *ngIf="normal_files_received" (selectionChange)="fileSelectionChanged($event)">
|
||||
<mat-list-option [selected]="selected_data.includes(file.uid)" *ngFor="let file of paged_data" [value]="file">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-10">
|
||||
<mat-icon class="audio-video-icon">{{(file.type === 'audio' || file.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon>
|
||||
{{file.title}}
|
||||
</div>
|
||||
<div class="col-2">{{file.registered | date:'shortDate'}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</mat-list-option>
|
||||
</mat-selection-list>
|
||||
|
||||
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
|
||||
<mat-selection-list *ngIf="!normal_files_received">
|
||||
<mat-list-option *ngFor="let file of paged_data">
|
||||
<content-loader class="list-ghosts" [primaryColor]="postsService.theme.ghost_primary" [secondaryColor]="postsService.theme.ghost_secondary" width="250" height="8"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="8" /></content-loader>
|
||||
</mat-list-option>
|
||||
</mat-selection-list>
|
||||
</ng-container>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
|
||||
<div style="position: relative;" *ngIf="usePaginator && selectedIndex > 0">
|
||||
<div style="position: absolute; margin-left: 8px; margin-top: 5px; scale: 0.8">
|
||||
<mat-form-field>
|
||||
<mat-label><ng-container i18n="File type">File type</ng-container></mat-label>
|
||||
@@ -57,7 +106,7 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<mat-paginator class="paginator" #paginator *ngIf="paged_data && paged_data.length > 0" (page)="pageChangeEvent($event)" [length]="file_count"
|
||||
<mat-paginator class="paginator" #paginator (page)="pageChangeEvent($event)" [length]="file_count"
|
||||
[pageSize]="pageSize"
|
||||
[pageSizeOptions]="[5, 10, 25, 100, this.paged_data && this.paged_data.length > 100 ? this.paged_data.length : 250]">
|
||||
</mat-paginator>
|
||||
|
||||
@@ -61,4 +61,61 @@
|
||||
.my-videos-title {
|
||||
top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.list-ghosts {
|
||||
position: relative;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.audio-video-icon {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
.cdk-drag-preview {
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
|
||||
0 8px 10px 1px rgba(0, 0, 0, 0.14),
|
||||
0 3px 14px 2px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.cdk-drag-placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.cdk-drag-animating {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.media-box:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.media-list.cdk-drop-list-dragging .media-box:not(.cdk-drag-placeholder) {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.remove-item-button {
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.playlist-item-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 70%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.blurred {
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.downloading-spinner {
|
||||
align-self: center;
|
||||
position: absolute;
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { Router } from '@angular/router';
|
||||
import { FileType } from '../../../api-types';
|
||||
import { DatabaseFile, FileType, FileTypeFilter } from '../../../api-types';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { Subject } from 'rxjs';
|
||||
import { distinctUntilChanged } from 'rxjs/operators';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recent-videos',
|
||||
@@ -13,6 +14,26 @@ import { distinctUntilChanged } from 'rxjs/operators';
|
||||
})
|
||||
export class RecentVideosComponent implements OnInit {
|
||||
|
||||
@Input() usePaginator = true;
|
||||
|
||||
// File selection
|
||||
|
||||
@Input() selectMode = false;
|
||||
@Input() defaultSelected: DatabaseFile[] = [];
|
||||
@Input() sub_id = null;
|
||||
@Input() customHeader = null;
|
||||
@Input() selectedIndex = 1;
|
||||
@Output() fileSelectionEmitter = new EventEmitter<{new_selection: string[], thumbnailURL: string}>();
|
||||
|
||||
pageSize = 10;
|
||||
paged_data: DatabaseFile[] = null;
|
||||
|
||||
selected_data: string[] = [];
|
||||
selected_data_objs: DatabaseFile[] = [];
|
||||
reverse_order = false;
|
||||
|
||||
// File listing (with cards)
|
||||
|
||||
cached_file_count = 0;
|
||||
loading_files = null;
|
||||
|
||||
@@ -20,7 +41,7 @@ export class RecentVideosComponent implements OnInit {
|
||||
subscription_files_received = false;
|
||||
file_count = 10;
|
||||
searchChangedSubject: Subject<string> = new Subject<string>();
|
||||
downloading_content = {'video': {}, 'audio': {}};
|
||||
downloading_content = {};
|
||||
search_mode = false;
|
||||
search_text = '';
|
||||
searchIsFocused = false;
|
||||
@@ -57,18 +78,32 @@ export class RecentVideosComponent implements OnInit {
|
||||
|
||||
playlists = null;
|
||||
|
||||
pageSize = 10;
|
||||
paged_data = null;
|
||||
|
||||
@ViewChild('paginator') paginator: MatPaginator
|
||||
|
||||
constructor(public postsService: PostsService, private router: Router) {
|
||||
// get cached file count
|
||||
if (localStorage.getItem('cached_file_count')) {
|
||||
this.cached_file_count = +localStorage.getItem('cached_file_count') <= 10 ? +localStorage.getItem('cached_file_count') : 10;
|
||||
|
||||
this.loading_files = Array(this.cached_file_count).fill(0);
|
||||
}
|
||||
|
||||
// set filter property to cached value
|
||||
const cached_filter_property = localStorage.getItem('filter_property');
|
||||
if (cached_filter_property && this.filterProperties[cached_filter_property]) {
|
||||
this.filterProperty = this.filterProperties[cached_filter_property];
|
||||
}
|
||||
|
||||
// set file type filter to cached value
|
||||
const cached_file_type_filter = localStorage.getItem('file_type_filter');
|
||||
if (this.usePaginator && cached_file_type_filter) {
|
||||
this.fileTypeFilter = cached_file_type_filter;
|
||||
}
|
||||
|
||||
const sort_order = localStorage.getItem('recent_videos_sort_order');
|
||||
|
||||
if (sort_order) {
|
||||
this.descendingMode = sort_order === 'descending';
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -96,23 +131,9 @@ export class RecentVideosComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
|
||||
// set filter property to cached value
|
||||
const cached_filter_property = localStorage.getItem('filter_property');
|
||||
if (cached_filter_property && this.filterProperties[cached_filter_property]) {
|
||||
this.filterProperty = this.filterProperties[cached_filter_property];
|
||||
}
|
||||
|
||||
// set file type filter to cached value
|
||||
const cached_file_type_filter = localStorage.getItem('file_type_filter');
|
||||
if (cached_file_type_filter) {
|
||||
this.fileTypeFilter = cached_file_type_filter;
|
||||
}
|
||||
|
||||
const sort_order = localStorage.getItem('recent_videos_sort_order');
|
||||
|
||||
if (sort_order) {
|
||||
this.descendingMode = sort_order === 'descending';
|
||||
}
|
||||
|
||||
this.selected_data = this.defaultSelected.map(file => file.uid);
|
||||
this.selected_data_objs = this.defaultSelected;
|
||||
|
||||
this.searchChangedSubject
|
||||
.debounceTime(500)
|
||||
@@ -127,7 +148,7 @@ export class RecentVideosComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
getAllPlaylists() {
|
||||
getAllPlaylists(): void {
|
||||
this.postsService.getPlaylists().subscribe(res => {
|
||||
this.playlists = res['playlists'];
|
||||
});
|
||||
@@ -135,22 +156,22 @@ export class RecentVideosComponent implements OnInit {
|
||||
|
||||
// search
|
||||
|
||||
onSearchInputChanged(newvalue) {
|
||||
onSearchInputChanged(newvalue: string): void {
|
||||
this.normal_files_received = false;
|
||||
this.searchChangedSubject.next(newvalue);
|
||||
}
|
||||
|
||||
filterOptionChanged(value) {
|
||||
filterOptionChanged(value: string): void {
|
||||
localStorage.setItem('filter_property', value['key']);
|
||||
this.getAllFiles();
|
||||
}
|
||||
|
||||
fileTypeFilterChanged(value) {
|
||||
fileTypeFilterChanged(value: string): void {
|
||||
localStorage.setItem('file_type_filter', value);
|
||||
this.getAllFiles();
|
||||
}
|
||||
|
||||
toggleModeChange() {
|
||||
toggleModeChange(): void {
|
||||
this.descendingMode = !this.descendingMode;
|
||||
localStorage.setItem('recent_videos_sort_order', this.descendingMode ? 'descending' : 'ascending');
|
||||
this.getAllFiles();
|
||||
@@ -158,12 +179,12 @@ export class RecentVideosComponent implements OnInit {
|
||||
|
||||
// get files
|
||||
|
||||
getAllFiles(cache_mode = false) {
|
||||
getAllFiles(cache_mode = false): void {
|
||||
this.normal_files_received = cache_mode;
|
||||
const current_file_index = (this.paginator?.pageIndex ? this.paginator.pageIndex : 0)*this.pageSize;
|
||||
const sort = {by: this.filterProperty['property'], order: this.descendingMode ? -1 : 1};
|
||||
const range = [current_file_index, current_file_index + this.pageSize];
|
||||
this.postsService.getAllFiles(sort, range, this.search_mode ? this.search_text : null, this.fileTypeFilter).subscribe(res => {
|
||||
this.postsService.getAllFiles(sort, range, this.search_mode ? this.search_text : null, this.fileTypeFilter as FileTypeFilter, this.sub_id).subscribe(res => {
|
||||
this.file_count = res['file_count'];
|
||||
this.paged_data = res['files'];
|
||||
for (let i = 0; i < this.paged_data.length; i++) {
|
||||
@@ -191,21 +212,12 @@ export class RecentVideosComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
navigateToFile(file, new_tab) {
|
||||
navigateToFile(file: DatabaseFile, new_tab: boolean): void {
|
||||
localStorage.setItem('player_navigator', this.router.url);
|
||||
if (file.sub_id) {
|
||||
const sub = this.postsService.getSubscriptionByID(file.sub_id);
|
||||
if (sub.streamingOnly) {
|
||||
// streaming only mode subscriptions
|
||||
// !new_tab ? this.router.navigate(['/player', {name: file.id,
|
||||
// url: file.requested_formats ? file.requested_formats[0].url : file.url}])
|
||||
// : window.open(`/#/player;name=${file.id};url=${file.requested_formats ? file.requested_formats[0].url : file.url}`);
|
||||
} else {
|
||||
// normal subscriptions
|
||||
!new_tab ? this.router.navigate(['/player', {uid: file.uid,
|
||||
type: file.isAudio ? 'audio' : 'video'}])
|
||||
type: file.isAudio ? 'audio' : 'video'}])
|
||||
: window.open(`/#/player;uid=${file.uid};type=${file.isAudio ? 'audio' : 'video'}`);
|
||||
}
|
||||
} else {
|
||||
// normal files
|
||||
!new_tab ? this.router.navigate(['/player', {type: file.isAudio ? 'audio' : 'video', uid: file.uid}])
|
||||
@@ -213,46 +225,26 @@ export class RecentVideosComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
goToSubscription(file) {
|
||||
goToSubscription(file: DatabaseFile): void {
|
||||
this.router.navigate(['/subscription', {id: file.sub_id}]);
|
||||
}
|
||||
|
||||
// downloading
|
||||
|
||||
downloadFile(file) {
|
||||
if (file.sub_id) {
|
||||
this.downloadSubscriptionFile(file);
|
||||
} else {
|
||||
this.downloadNormalFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
downloadSubscriptionFile(file) {
|
||||
const type = (file.isAudio ? 'audio' : 'video') as FileType;
|
||||
const ext = type === 'audio' ? '.mp3' : '.mp4'
|
||||
const sub = this.postsService.getSubscriptionByID(file.sub_id);
|
||||
this.postsService.downloadFileFromServer(file.uid).subscribe(res => {
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, file.id + ext);
|
||||
}, err => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
downloadNormalFile(file) {
|
||||
downloadFile(file: DatabaseFile): void {
|
||||
const type = (file.isAudio ? 'audio' : 'video') as FileType;
|
||||
const ext = type === 'audio' ? '.mp3' : '.mp4'
|
||||
const name = file.id;
|
||||
this.downloading_content[type][name] = true;
|
||||
this.downloading_content[file.uid] = true;
|
||||
this.postsService.downloadFileFromServer(file.uid).subscribe(res => {
|
||||
this.downloading_content[type][name] = false;
|
||||
this.downloading_content[file.uid] = false;
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, decodeURIComponent(name) + ext);
|
||||
|
||||
if (!this.postsService.config.Extra.file_manager_enabled) {
|
||||
if (!this.postsService.config.Extra.file_manager_enabled && !file.sub_id) {
|
||||
// tell server to delete the file once downloaded
|
||||
this.postsService.deleteFile(file.uid).subscribe(delRes => {
|
||||
// reload mp4s
|
||||
this.postsService.deleteFile(file.uid).subscribe(() => {
|
||||
// reload files
|
||||
this.getAllFiles();
|
||||
});
|
||||
}
|
||||
@@ -263,7 +255,6 @@ export class RecentVideosComponent implements OnInit {
|
||||
|
||||
deleteFile(args) {
|
||||
const file = args.file;
|
||||
const index = args.index;
|
||||
const blacklistMode = args.blacklistMode;
|
||||
|
||||
if (file.sub_id) {
|
||||
@@ -273,20 +264,20 @@ export class RecentVideosComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
deleteNormalFile(file, blacklistMode = false) {
|
||||
deleteNormalFile(file: DatabaseFile, blacklistMode = false): void {
|
||||
this.postsService.deleteFile(file.uid, blacklistMode).subscribe(result => {
|
||||
if (result) {
|
||||
this.postsService.openSnackBar('Delete success!', 'OK.');
|
||||
this.postsService.openSnackBar($localize`Delete success!`, $localize`OK.`);
|
||||
this.removeFileCard(file);
|
||||
} else {
|
||||
this.postsService.openSnackBar('Delete failed!', 'OK.');
|
||||
this.postsService.openSnackBar($localize`Delete failed!`, $localize`OK.`);
|
||||
}
|
||||
}, err => {
|
||||
this.postsService.openSnackBar('Delete failed!', 'OK.');
|
||||
}, () => {
|
||||
this.postsService.openSnackBar($localize`Delete failed!`, $localize`OK.`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteSubscriptionFile(file, blacklistMode = false) {
|
||||
deleteSubscriptionFile(file: DatabaseFile, blacklistMode = false): void {
|
||||
if (blacklistMode) {
|
||||
this.deleteForever(file);
|
||||
} else {
|
||||
@@ -294,28 +285,29 @@ export class RecentVideosComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
deleteAndRedownload(file) {
|
||||
deleteAndRedownload(file: DatabaseFile): void {
|
||||
const sub = this.postsService.getSubscriptionByID(file.sub_id);
|
||||
this.postsService.deleteSubscriptionFile(sub, file.id, false, file.uid).subscribe(res => {
|
||||
this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`);
|
||||
this.postsService.deleteSubscriptionFile(sub, file.id, false, file.uid).subscribe(() => {
|
||||
this.postsService.openSnackBar($localize`Successfully deleted file: ` + file.id);
|
||||
this.removeFileCard(file);
|
||||
});
|
||||
}
|
||||
|
||||
deleteForever(file) {
|
||||
deleteForever(file: DatabaseFile): void {
|
||||
const sub = this.postsService.getSubscriptionByID(file.sub_id);
|
||||
this.postsService.deleteSubscriptionFile(sub, file.id, true, file.uid).subscribe(res => {
|
||||
this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`);
|
||||
this.postsService.deleteSubscriptionFile(sub, file.id, true, file.uid).subscribe(() => {
|
||||
this.postsService.openSnackBar($localize`Successfully deleted file: ` + file.id);
|
||||
this.removeFileCard(file);
|
||||
});
|
||||
}
|
||||
|
||||
removeFileCard(file_to_remove) {
|
||||
removeFileCard(file_to_remove: DatabaseFile): void {
|
||||
const index = this.paged_data.map(e => e.uid).indexOf(file_to_remove.uid);
|
||||
this.paged_data.splice(index, 1);
|
||||
this.getAllFiles(true);
|
||||
}
|
||||
|
||||
// TODO: Add translation support for these snackbars
|
||||
addFileToPlaylist(info_obj) {
|
||||
const file = info_obj['file'];
|
||||
const playlist_id = info_obj['playlist_id'];
|
||||
@@ -335,13 +327,13 @@ export class RecentVideosComponent implements OnInit {
|
||||
|
||||
// sorting and filtering
|
||||
|
||||
sortFiles(a, b) {
|
||||
sortFiles(a: DatabaseFile, b: DatabaseFile): number {
|
||||
// uses the 'registered' flag as the timestamp
|
||||
const result = b.registered - a.registered;
|
||||
return result;
|
||||
}
|
||||
|
||||
durationStringToNumber(dur_str) {
|
||||
durationStringToNumber(dur_str: string): number {
|
||||
let num_sum = 0;
|
||||
const dur_str_parts = dur_str.split(':');
|
||||
for (let i = dur_str_parts.length - 1; i >= 0; i--) {
|
||||
@@ -355,4 +347,42 @@ export class RecentVideosComponent implements OnInit {
|
||||
this.loading_files = Array(this.pageSize).fill(0);
|
||||
this.getAllFiles();
|
||||
}
|
||||
|
||||
fileSelectionChanged(event: { option: { _selected: boolean; value: DatabaseFile; } }): void {
|
||||
const adding = event.option._selected;
|
||||
const value = event.option.value;
|
||||
if (adding) {
|
||||
this.selected_data.push(value.uid);
|
||||
this.selected_data_objs.push(value);
|
||||
} else {
|
||||
this.selected_data = this.selected_data.filter(e => e !== value.uid);
|
||||
this.selected_data_objs = this.selected_data_objs.filter(e => e.uid !== value.uid);
|
||||
}
|
||||
|
||||
this.fileSelectionEmitter.emit({new_selection: this.selected_data, thumbnailURL: this.selected_data_objs[0].thumbnailURL});
|
||||
}
|
||||
|
||||
toggleSelectionOrder(): void {
|
||||
this.reverse_order = !this.reverse_order;
|
||||
localStorage.setItem('default_playlist_order_reversed', '' + this.reverse_order);
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<string[]>): void {
|
||||
if (this.reverse_order) {
|
||||
event.previousIndex = this.selected_data.length - 1 - event.previousIndex;
|
||||
event.currentIndex = this.selected_data.length - 1 - event.currentIndex;
|
||||
}
|
||||
moveItemInArray(this.selected_data, event.previousIndex, event.currentIndex);
|
||||
moveItemInArray(this.selected_data_objs, event.previousIndex, event.currentIndex);
|
||||
this.fileSelectionEmitter.emit({new_selection: this.selected_data, thumbnailURL: this.selected_data_objs[0].thumbnailURL});
|
||||
}
|
||||
|
||||
removeSelectedFile(index: number): void {
|
||||
if (this.reverse_order) {
|
||||
index = this.selected_data.length - 1 - index;
|
||||
}
|
||||
this.selected_data.splice(index, 1);
|
||||
this.selected_data_objs.splice(index, 1);
|
||||
this.fileSelectionEmitter.emit({new_selection: this.selected_data, thumbnailURL: this.selected_data_objs[0].thumbnailURL});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialo
|
||||
import { RestoreDbDialogComponent } from 'app/dialogs/restore-db-dialog/restore-db-dialog.component';
|
||||
import { UpdateTaskScheduleDialogComponent } from 'app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { Task } from 'api-types';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tasks',
|
||||
@@ -17,7 +18,7 @@ export class TasksComponent implements OnInit {
|
||||
|
||||
interval_id = null;
|
||||
tasks_check_interval = 1500;
|
||||
tasks = null;
|
||||
tasks: Task[] = null;
|
||||
tasks_retrieved = false;
|
||||
|
||||
displayedColumns: string[] = ['title', 'last_ran', 'last_confirmed', 'status', 'actions'];
|
||||
@@ -55,6 +56,11 @@ export class TasksComponent implements OnInit {
|
||||
|
||||
getTasks(): void {
|
||||
this.postsService.getTasks().subscribe(res => {
|
||||
for (const task of res['tasks']) {
|
||||
if (task.title.includes('youtube-dl')) {
|
||||
task.title = task.title.replace('youtube-dl', this.postsService.config.Advanced.default_downloader);
|
||||
}
|
||||
}
|
||||
if (this.tasks) {
|
||||
if (JSON.stringify(this.tasks) === JSON.stringify(res['tasks'])) return;
|
||||
for (const task of res['tasks']) {
|
||||
@@ -94,7 +100,7 @@ export class TasksComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
scheduleTask(task: any): void {
|
||||
scheduleTask(task: Task): void {
|
||||
// open dialog
|
||||
const dialogRef = this.dialog.open(UpdateTaskScheduleDialogComponent, {
|
||||
data: {
|
||||
@@ -152,13 +158,3 @@ export class TasksComponent implements OnInit {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface Task {
|
||||
key: string;
|
||||
title: string;
|
||||
last_ran: number;
|
||||
last_confirmed: number;
|
||||
running: boolean;
|
||||
confirming: boolean;
|
||||
data: unknown;
|
||||
}
|
||||
@@ -96,18 +96,18 @@ export class TwitchChatComponent implements OnInit, OnDestroy {
|
||||
let vodId = this.db_file.url.split('videos/').length > 1 && this.db_file.url.split('videos/')[1];
|
||||
vodId = vodId.split('?')[0];
|
||||
if (!vodId) {
|
||||
this.postsService.openSnackBar('VOD url for this video is not supported. VOD ID must be after "twitch.tv/videos/"');
|
||||
this.postsService.openSnackBar($localize`VOD url for this video is not supported. VOD ID must be after "twitch.tv/videos/"`);
|
||||
}
|
||||
this.postsService.downloadTwitchChat(this.db_file.id, this.db_file.isAudio ? 'audio' : 'video', vodId, null, this.sub).subscribe(res => {
|
||||
if (res['chat']) {
|
||||
this.initializeChatCheck(res['chat']);
|
||||
} else {
|
||||
this.downloading_chat = false;
|
||||
this.postsService.openSnackBar('Download failed.')
|
||||
this.postsService.openSnackBar($localize`Download failed.`)
|
||||
}
|
||||
}, err => {
|
||||
this.downloading_chat = false;
|
||||
this.postsService.openSnackBar('Chat could not be downloaded.')
|
||||
this.postsService.openSnackBar($localize`Chat could not be downloaded.`)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<ng-container *ngIf="!is_playlist && !loading">
|
||||
<button (click)="openFileInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
|
||||
<button (click)="navigateToSubscription()" mat-menu-item *ngIf="file_obj.sub_id"><mat-icon>{{file_obj.isAudio ? 'library_music' : 'video_library'}}</mat-icon> <ng-container i18n="Go to subscription menu item">Go to subscription</ng-container></button>
|
||||
<button *ngIf="availablePlaylists" [matMenuTriggerFor]="addtoplaylist" mat-menu-item><mat-icon>playlist_add</mat-icon> <ng-container i18n="Add to playlist menu item">Add to playlist</ng-container></button>
|
||||
<button [disabled]="!availablePlaylists || availablePlaylists.length === 0" [matMenuTriggerFor]="addtoplaylist" mat-menu-item><mat-icon>playlist_add</mat-icon> <ng-container i18n="Add to playlist menu item">Add to playlist</ng-container></button>
|
||||
<mat-menu #addtoplaylist="matMenu">
|
||||
<ng-container *ngFor="let playlist of availablePlaylists">
|
||||
<button *ngIf="(playlist.type === 'audio') === file_obj.isAudio" [disabled]="playlist.uids?.includes(file_obj.uid)" (click)="emitAddFileToPlaylist(playlist.id)" mat-menu-item>{{playlist.name}}</button>
|
||||
@@ -34,10 +34,10 @@
|
||||
<mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container>
|
||||
</button>
|
||||
<button *ngIf="file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item>
|
||||
<mat-icon>delete_forever</mat-icon><ng-container i18n="Delete forever subscription video button">Delete forever</ng-container>
|
||||
<mat-icon>delete_forever</mat-icon><ng-container i18n="Delete forever subscription video button">Delete and don't download again</ng-container>
|
||||
</button>
|
||||
<button *ngIf="!file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
|
||||
<button *ngIf="!file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and blacklist video button">Delete and blacklist</ng-container></button>
|
||||
<button *ngIf="!file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and don't download again">Delete and don't download again</ng-container></button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="is_playlist && !loading">
|
||||
<button (click)="emitEditPlaylist()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button>
|
||||
|
||||
@@ -9,7 +9,6 @@ import localeES from '@angular/common/locales/es';
|
||||
import localeDE from '@angular/common/locales/de';
|
||||
import localeZH from '@angular/common/locales/zh';
|
||||
import localeNB from '@angular/common/locales/nb';
|
||||
import { DatabaseFile, Playlist } from 'api-types';
|
||||
|
||||
registerLocaleData(localeGB);
|
||||
registerLocaleData(localeFR);
|
||||
@@ -105,12 +104,16 @@ export class UnifiedFileCardComponent implements OnInit {
|
||||
}
|
||||
|
||||
openFileInfoDialog() {
|
||||
this.dialog.open(VideoInfoDialogComponent, {
|
||||
const dialogRef = this.dialog.open(VideoInfoDialogComponent, {
|
||||
data: {
|
||||
file: this.file_obj,
|
||||
},
|
||||
minWidth: '50vw'
|
||||
})
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.file_obj = dialogRef.componentInstance.file;
|
||||
});
|
||||
}
|
||||
|
||||
emitEditPlaylist() {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const CURRENT_VERSION = 'v4.2';
|
||||
export const CURRENT_VERSION = 'v4.3';
|
||||
|
||||
@@ -1,36 +1,29 @@
|
||||
<h4 mat-dialog-title i18n="Create a playlist dialog title">Create a playlist</h4>
|
||||
<form>
|
||||
<div *ngIf="filesToSelectFrom || (audiosToSelectFrom && videosToSelectFrom)">
|
||||
<div>
|
||||
<mat-form-field color="accent">
|
||||
<input [(ngModel)]="name" matInput placeholder="Name" i18n-placeholder="Playlist name placeholder" type="text" required aria-required [ngModelOptions]="{standalone: true}">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div *ngIf="!filesToSelectFrom">
|
||||
<mat-form-field color="accent">
|
||||
<mat-select placeholder="Type" i18n-placeholder="Type select" [(ngModel)]="type" [ngModelOptions]="{standalone: true}">
|
||||
<mat-option value="audio"><ng-container i18n="Audio">Audio</ng-container></mat-option>
|
||||
<mat-option value="video"><ng-container i18n="Video">Video</ng-container></mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field *ngIf="type && ((filesToSelectFrom && filesToSelectFrom.length > 0) || (type === 'audio' && audiosToSelectFrom && audiosToSelectFrom.length > 0) || (type === 'video' && videosToSelectFrom && videosToSelectFrom.length > 0))" color="accent">
|
||||
<mat-label *ngIf="type === 'audio'"><ng-container i18n="Audio files title">Audio files</ng-container></mat-label>
|
||||
<mat-label *ngIf="type === 'video'"><ng-container i18n="Videos title">Videos</ng-container></mat-label>
|
||||
<mat-select [formControl]="filesSelect" multiple required aria-required>
|
||||
<ng-container *ngIf="filesToSelectFrom"><mat-option *ngFor="let file of filesToSelectFrom" [value]="file.uid">{{file.id}}</mat-option></ng-container>
|
||||
<ng-container *ngIf="audiosToSelectFrom && type === 'audio'"><mat-option *ngFor="let file of audiosToSelectFrom" [value]="file.uid">{{file.id}}</mat-option></ng-container>
|
||||
<ng-container *ngIf="videosToSelectFrom && type === 'video'"><mat-option *ngFor="let file of videosToSelectFrom" [value]="file.uid">{{file.id}}</mat-option></ng-container>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<!-- No videos available -->
|
||||
<div style="margin-bottom: 15px;" *ngIf="type && ((filesToSelectFrom && filesToSelectFrom.length === 0) || (type === 'audio' && audiosToSelectFrom && audiosToSelectFrom.length === 0) || (type === 'video' && videosToSelectFrom && videosToSelectFrom.length === 0))">
|
||||
No files available.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="fixActionRow">
|
||||
<h4 mat-dialog-title *ngIf="create_mode" ><ng-container i18n="Create a playlist dialog title">Create a playlist</ng-container></h4>
|
||||
<h4 mat-dialog-title *ngIf="!create_mode"><ng-container i18n="Modify playlist dialog title">Modify playlist</ng-container></h4>
|
||||
|
||||
<div *ngIf="create_in_progress" style="float: left"><mat-spinner [diameter]="25"></mat-spinner></div>
|
||||
<button (click)="createPlaylist()" [disabled]="!name || !filesSelect.value || filesSelect.value.length === 0" color="primary" style="float: right" mat-flat-button>Create</button>
|
||||
<mat-dialog-content style="max-height: 85vh;">
|
||||
<form>
|
||||
<div *ngIf="create_mode || playlist">
|
||||
<div>
|
||||
<mat-form-field color="accent">
|
||||
<input [(ngModel)]="name" matInput placeholder="Name" i18n-placeholder="Playlist name placeholder" type="text" required aria-required [ngModelOptions]="{standalone: true}">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<app-recent-videos [selectMode]="true" [defaultSelected]="preselected_files" [customHeader]="'Select files'" (fileSelectionEmitter)="fileSelectionChanged($event)" [selectedIndex]="create_mode ? 1 : 0"></app-recent-videos>
|
||||
</div>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<mat-dialog-actions>
|
||||
<button *ngIf="create_mode" (click)="createPlaylist()" [disabled]="!name || !filesSelect.value || filesSelect.value.length === 0" color="primary" style="float: right" mat-flat-button>
|
||||
<ng-container i18n="Create button">Create</ng-container>
|
||||
</button>
|
||||
<button *ngIf="!create_mode" (click)="updatePlaylist()" [disabled]="!name || !playlistChanged()" color="primary" style="float: right" mat-flat-button>
|
||||
<ng-container i18n="Save button">Save</ng-container>
|
||||
</button>
|
||||
<div *ngIf="create_in_progress" style="margin-left: 10px"><mat-spinner [diameter]="25"></mat-spinner></div>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
.fixActionRow {
|
||||
height: 89vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { UntypedFormControl } from '@angular/forms';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { Playlist } from 'api-types';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-playlist',
|
||||
@@ -9,72 +10,94 @@ import { PostsService } from 'app/posts.services';
|
||||
styleUrls: ['./create-playlist.component.scss']
|
||||
})
|
||||
export class CreatePlaylistComponent implements OnInit {
|
||||
// really "createPlaylistDialogComponent"
|
||||
// really "createAndModifyPlaylistDialogComponent"
|
||||
|
||||
filesToSelectFrom = null;
|
||||
type = null;
|
||||
filesSelect = new FormControl();
|
||||
filesSelect = new UntypedFormControl();
|
||||
audiosToSelectFrom = null;
|
||||
videosToSelectFrom = null;
|
||||
name = '';
|
||||
cached_thumbnail_url = null;
|
||||
|
||||
create_in_progress = false;
|
||||
create_mode = false;
|
||||
|
||||
// playlist modify mode
|
||||
|
||||
playlist: Playlist = null;
|
||||
playlist_id: string = null;
|
||||
preselected_files = [];
|
||||
playlist_updated = false;
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private postsService: PostsService,
|
||||
public dialogRef: MatDialogRef<CreatePlaylistComponent>) { }
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
if (this.data) {
|
||||
this.filesToSelectFrom = this.data.filesToSelectFrom;
|
||||
this.type = this.data.type;
|
||||
}
|
||||
|
||||
if (!this.filesToSelectFrom) {
|
||||
this.getMp3s();
|
||||
this.getMp4s();
|
||||
}
|
||||
public dialogRef: MatDialogRef<CreatePlaylistComponent>) {
|
||||
if (this.data?.create_mode) this.create_mode = true;
|
||||
if (this.data?.playlist_id) {
|
||||
this.playlist_id = this.data.playlist_id;
|
||||
this.getPlaylist();
|
||||
}
|
||||
}
|
||||
|
||||
getMp3s() {
|
||||
this.postsService.getMp3s().subscribe(result => {
|
||||
this.audiosToSelectFrom = result['mp3s'];
|
||||
});
|
||||
}
|
||||
|
||||
getMp4s() {
|
||||
this.postsService.getMp4s().subscribe(result => {
|
||||
this.videosToSelectFrom = result['mp4s'];
|
||||
});
|
||||
}
|
||||
ngOnInit(): void {}
|
||||
|
||||
createPlaylist() {
|
||||
createPlaylist(): void {
|
||||
const thumbnailURL = this.getThumbnailURL();
|
||||
this.create_in_progress = true;
|
||||
this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL).subscribe(res => {
|
||||
this.postsService.createPlaylist(this.name, this.filesSelect.value, thumbnailURL).subscribe(res => {
|
||||
this.create_in_progress = false;
|
||||
if (res['success']) {
|
||||
this.dialogRef.close(true);
|
||||
} else {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}, err => {
|
||||
this.create_in_progress = false;
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
getThumbnailURL() {
|
||||
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 === this.filesSelect.value[0]) {
|
||||
// different services store the thumbnail in different places
|
||||
if (file.thumbnailURL) { return file.thumbnailURL };
|
||||
if (file.thumbnail) { return file.thumbnail };
|
||||
updatePlaylist(): void {
|
||||
this.create_in_progress = true;
|
||||
this.playlist['name'] = this.name;
|
||||
this.playlist['uids'] = this.filesSelect.value;
|
||||
this.playlist_updated = true;
|
||||
this.postsService.updatePlaylist(this.playlist).subscribe(() => {
|
||||
this.create_in_progress = false;
|
||||
this.postsService.openSnackBar($localize`Playlist updated successfully.`);
|
||||
this.getPlaylist();
|
||||
this.postsService.playlists_changed.next(true);
|
||||
}, err => {
|
||||
this.create_in_progress = false;
|
||||
console.error(err)
|
||||
this.postsService.openSnackBar($localize`Playlist updated successfully.`);
|
||||
});
|
||||
}
|
||||
|
||||
getThumbnailURL(): string {
|
||||
return this.cached_thumbnail_url;
|
||||
}
|
||||
|
||||
fileSelectionChanged({new_selection, thumbnailURL}: {new_selection: string[], thumbnailURL: string}): void {
|
||||
this.filesSelect.setValue(new_selection);
|
||||
if (new_selection.length) this.cached_thumbnail_url = thumbnailURL;
|
||||
else this.cached_thumbnail_url = null;
|
||||
}
|
||||
|
||||
playlistChanged(): boolean {
|
||||
return JSON.stringify(this.playlist.uids) !== JSON.stringify(this.filesSelect.value) || this.name !== this.playlist.name;
|
||||
}
|
||||
|
||||
getPlaylist(): void {
|
||||
this.postsService.getPlaylist(this.playlist_id, null, true).subscribe(res => {
|
||||
if (res['playlist']) {
|
||||
this.filesSelect.setValue(res['file_objs'].map(file => file.uid));
|
||||
this.preselected_files = res['file_objs'];
|
||||
this.playlist = res['playlist'];
|
||||
this.name = this.playlist['name'];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, OnInit, Inject, Pipe, PipeTransform, ViewChild, AfterViewIni
|
||||
import { COMMA, ENTER } from '@angular/cdk/keycodes';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef, MatDialog } from '@angular/material/dialog';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { UntypedFormControl } from '@angular/forms';
|
||||
import { args, args_info } from './youtubedl_args';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators/map';
|
||||
@@ -30,13 +30,13 @@ export class HighlightPipe implements PipeTransform {
|
||||
styleUrls: ['./arg-modifier-dialog.component.scss'],
|
||||
})
|
||||
export class ArgModifierDialogComponent implements OnInit, AfterViewInit {
|
||||
myGroup = new FormControl();
|
||||
myGroup = new UntypedFormControl();
|
||||
firstArg = '';
|
||||
secondArg = '';
|
||||
secondArgEnabled = false;
|
||||
modified_args = '';
|
||||
stateCtrl = new FormControl();
|
||||
chipCtrl = new FormControl();
|
||||
stateCtrl = new UntypedFormControl();
|
||||
chipCtrl = new UntypedFormControl();
|
||||
availableArgs = null;
|
||||
argsByCategory = null;
|
||||
argsByKey = null;
|
||||
|
||||
@@ -39,7 +39,7 @@ export class CookiesUploaderDialogComponent implements OnInit {
|
||||
this.uploading = false;
|
||||
if (res['success']) {
|
||||
this.uploaded = true;
|
||||
this.postsService.openSnackBar('Cookies successfully uploaded!');
|
||||
this.postsService.openSnackBar($localize`Cookies successfully uploaded!`);
|
||||
}
|
||||
}, err => {
|
||||
this.uploading = false;
|
||||
|
||||
@@ -34,11 +34,6 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-1">
|
||||
<div>
|
||||
<mat-checkbox [disabled]="new_sub.type === 'audio'" [(ngModel)]="new_sub.streamingOnly"><ng-container i18n="Streaming-only mode">Streaming-only mode</ng-container></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 mb-3">
|
||||
<mat-form-field color="accent">
|
||||
<input [(ngModel)]="new_sub.custom_args" matInput placeholder="Custom args" i18n-placeholder="Subscription custom args placeholder">
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<h4 mat-dialog-title><ng-container i18n="Modify playlist dialog title">Modify playlist</ng-container></h4>
|
||||
|
||||
<mat-dialog-content>
|
||||
<div *ngIf="playlist">
|
||||
<!-- Playlist info -->
|
||||
<div>
|
||||
<mat-form-field color="accent">
|
||||
<input matInput placeholder="Name" i18n-placeholder="Name" [(ngModel)]="playlist.name">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-checkbox [(ngModel)]="playlist.randomize_order"><ng-container i18n="Randomize order when playing checkbox label">Randomize order when playing</ng-container></mat-checkbox>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 10px; height: 40px;">
|
||||
<div style="float: left">
|
||||
<span *ngIf="reverse_order === false" i18n="Normal order">Normal order </span>
|
||||
<span *ngIf="reverse_order === true" i18n="Reverse order">Reverse order </span>
|
||||
<button (click)="togglePlaylistOrder()" mat-icon-button><mat-icon>{{!reverse_order ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
|
||||
</div>
|
||||
|
||||
<div style="float: right">
|
||||
<button [disabled]="available_files.length === 0" mat-stroked-button [matMenuTriggerFor]="menu"><ng-container i18n="Add content">Add content</ng-container></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Playlist order -->
|
||||
<mat-button-toggle-group class="media-list" cdkDropList (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
|
||||
<!-- The following for loop can be optimized but it requires a pipe https://stackoverflow.com/a/35703364/8088021 -->
|
||||
<mat-button-toggle class="media-box" cdkDrag *ngFor="let playlist_item of (reverse_order ? playlist_file_objs.slice().reverse() : playlist_file_objs); let i = index" [checked]="false"><div><div class="playlist-item-text">{{playlist_item.title}}</div> <button (click)="removeContent(i)" class="remove-item-button" mat-icon-button><mat-icon>cancel</mat-icon></button></div></mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
|
||||
<mat-menu #menu="matMenu">
|
||||
<button *ngFor="let file of available_files" (click)="addContent(file)" mat-menu-item>{{file.title}}</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions>
|
||||
<!-- Save -->
|
||||
<button [disabled]="!playlist || !playlistChanged()" (click)="updatePlaylist()" mat-raised-button color="accent"><ng-container i18n="Save">Save</ng-container></button>
|
||||
</mat-dialog-actions>
|
||||
@@ -1,45 +0,0 @@
|
||||
.media-list {
|
||||
|
||||
}
|
||||
|
||||
.media-box {
|
||||
|
||||
}
|
||||
|
||||
.cdk-drag-preview {
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
|
||||
0 8px 10px 1px rgba(0, 0, 0, 0.14),
|
||||
0 3px 14px 2px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.cdk-drag-placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.cdk-drag-animating {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.media-box:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.media-list.cdk-drop-list-dragging .media-box:not(.cdk-drag-placeholder) {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.remove-item-button {
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.playlist-item-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 70%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { ModifyPlaylistComponent } from './modify-playlist.component';
|
||||
|
||||
describe('ModifyPlaylistComponent', () => {
|
||||
let component: ModifyPlaylistComponent;
|
||||
let fixture: ComponentFixture<ModifyPlaylistComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ModifyPlaylistComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ModifyPlaylistComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,107 +0,0 @@
|
||||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
|
||||
@Component({
|
||||
selector: 'app-modify-playlist',
|
||||
templateUrl: './modify-playlist.component.html',
|
||||
styleUrls: ['./modify-playlist.component.scss']
|
||||
})
|
||||
export class ModifyPlaylistComponent implements OnInit {
|
||||
|
||||
playlist_id = null;
|
||||
|
||||
original_playlist = null;
|
||||
playlist = null;
|
||||
playlist_file_objs = null;
|
||||
|
||||
available_files = [];
|
||||
all_files = [];
|
||||
playlist_updated = false;
|
||||
reverse_order = false;
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private postsService: PostsService,
|
||||
public dialogRef: MatDialogRef<ModifyPlaylistComponent>) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.data) {
|
||||
this.playlist_id = this.data.playlist_id;
|
||||
this.getPlaylist();
|
||||
}
|
||||
|
||||
this.reverse_order = localStorage.getItem('default_playlist_order_reversed') === 'true';
|
||||
}
|
||||
|
||||
getFiles() {
|
||||
if (this.playlist.type === 'audio') {
|
||||
this.postsService.getMp3s().subscribe(res => {
|
||||
this.processFiles(res['mp3s']);
|
||||
});
|
||||
} else {
|
||||
this.postsService.getMp4s().subscribe(res => {
|
||||
this.processFiles(res['mp4s']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
processFiles(new_files = null) {
|
||||
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.');
|
||||
this.getPlaylist();
|
||||
this.postsService.playlists_changed.next(true);
|
||||
});
|
||||
}
|
||||
|
||||
playlistChanged() {
|
||||
return JSON.stringify(this.playlist) !== JSON.stringify(this.original_playlist);
|
||||
}
|
||||
|
||||
getPlaylist() {
|
||||
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_file_objs.push(file);
|
||||
this.playlist.uids.push(file.uid);
|
||||
this.processFiles();
|
||||
}
|
||||
|
||||
removeContent(index) {
|
||||
if (this.reverse_order) {
|
||||
index = this.playlist_file_objs.length - 1 - index;
|
||||
}
|
||||
this.playlist_file_objs.splice(index, 1);
|
||||
this.playlist.uids.splice(index, 1);
|
||||
this.processFiles();
|
||||
}
|
||||
|
||||
togglePlaylistOrder() {
|
||||
this.reverse_order = !this.reverse_order;
|
||||
localStorage.setItem('default_playlist_order_reversed', '' + this.reverse_order);
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<string[]>) {
|
||||
if (this.reverse_order) {
|
||||
event.previousIndex = this.playlist_file_objs.length - 1 - event.previousIndex;
|
||||
event.currentIndex = this.playlist_file_objs.length - 1 - event.currentIndex;
|
||||
}
|
||||
moveItemInArray(this.playlist_file_objs, event.previousIndex, event.currentIndex);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -36,14 +36,14 @@ export class RestoreDbDialogComponent implements OnInit {
|
||||
this.postsService.restoreDBBackup(this.selected_backup[0]).subscribe(res => {
|
||||
this.restoring = false;
|
||||
if (res['success']) {
|
||||
this.postsService.openSnackBar('Database successfully restored!');
|
||||
this.postsService.openSnackBar($localize`Database successfully restored!`);
|
||||
this.dialogRef.close();
|
||||
} else {
|
||||
this.postsService.openSnackBar('Failed to restore database! See logs for more info.');
|
||||
this.postsService.openSnackBar($localize`Failed to restore database! See logs for more info.`);
|
||||
}
|
||||
}, err => {
|
||||
this.restoring = false;
|
||||
this.postsService.openSnackBar('Failed to restore database! See browser console for more info.');
|
||||
this.postsService.openSnackBar($localize`Failed to restore database! See browser console for more info.`);
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,31 +58,31 @@ export class ShareMediaDialogComponent implements OnInit {
|
||||
}
|
||||
|
||||
copiedToClipboard() {
|
||||
this.openSnackBar('Copied to clipboard!');
|
||||
this.postsService.openSnackBar($localize`Copied to clipboard!`);
|
||||
}
|
||||
|
||||
sharingChanged(event) {
|
||||
if (event.checked) {
|
||||
this.postsService.enableSharing(this.uid, this.is_playlist).subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.openSnackBar('Sharing enabled.');
|
||||
this.postsService.openSnackBar($localize`Sharing enabled.`);
|
||||
this.sharing_enabled = true;
|
||||
} else {
|
||||
this.openSnackBar('Failed to enable sharing.');
|
||||
this.postsService.openSnackBar($localize`Failed to enable sharing.`);
|
||||
}
|
||||
}, err => {
|
||||
this.openSnackBar('Failed to enable sharing - server error.');
|
||||
this.postsService.openSnackBar($localize`Failed to enable sharing - server error.`);
|
||||
});
|
||||
} else {
|
||||
this.postsService.disableSharing(this.uid, this.is_playlist).subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.openSnackBar('Sharing disabled.');
|
||||
this.postsService.openSnackBar($localize`Sharing disabled.`);
|
||||
this.sharing_enabled = false;
|
||||
} else {
|
||||
this.openSnackBar('Failed to disable sharing.');
|
||||
this.postsService.openSnackBar($localize`Failed to disable sharing.`);
|
||||
}
|
||||
}, err => {
|
||||
this.openSnackBar('Failed to disable sharing - server error.');
|
||||
this.postsService.openSnackBar($localize`Failed to disable sharing - server error.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,11 +47,6 @@
|
||||
<mat-checkbox [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<mat-checkbox [disabled]="audioOnlyMode" [(ngModel)]="streamingOnlyMode"><ng-container i18n="Streaming-only mode">Streaming-only mode</ng-container></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 mb-3">
|
||||
<mat-form-field color="accent">
|
||||
<input [(ngModel)]="customArgs" matInput placeholder="Custom args" i18n-placeholder="Subscription custom args placeholder">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatDialogRef, MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { ArgModifierDialogComponent } from '../arg-modifier-dialog/arg-modifier-dialog.component';
|
||||
|
||||
@@ -22,9 +21,6 @@ export class SubscribeDialogComponent implements OnInit {
|
||||
// state
|
||||
subscribing = false;
|
||||
|
||||
// no videos actually downloaded, just streamed
|
||||
streamingOnlyMode = false;
|
||||
|
||||
// audio only mode
|
||||
audioOnlyMode = false;
|
||||
|
||||
@@ -70,7 +66,6 @@ export class SubscribeDialogComponent implements OnInit {
|
||||
];
|
||||
|
||||
constructor(private postsService: PostsService,
|
||||
private snackBar: MatSnackBar,
|
||||
private dialog: MatDialog,
|
||||
public dialogRef: MatDialogRef<SubscribeDialogComponent>) { }
|
||||
|
||||
@@ -81,7 +76,7 @@ export class SubscribeDialogComponent implements OnInit {
|
||||
if (this.url && this.url !== '') {
|
||||
// timerange must be specified if download_all is false
|
||||
if (!this.download_all && !this.timerange_amount) {
|
||||
this.openSnackBar('You must specify an amount of time');
|
||||
this.postsService.openSnackBar($localize`You must specify an amount of time`);
|
||||
return;
|
||||
}
|
||||
this.subscribing = true;
|
||||
@@ -97,7 +92,7 @@ export class SubscribeDialogComponent implements OnInit {
|
||||
this.dialogRef.close(res['new_sub']);
|
||||
} else {
|
||||
if (res['error']) {
|
||||
this.openSnackBar('ERROR: ' + res['error']);
|
||||
this.postsService.openSnackBar($localize`ERROR: ` + res['error']);
|
||||
}
|
||||
this.dialogRef.close();
|
||||
}
|
||||
@@ -118,11 +113,4 @@ export class SubscribeDialogComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public openSnackBar(message: string, action = '') {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { UpdaterStatus } from '../../../api-types';
|
||||
|
||||
@Component({
|
||||
@@ -14,7 +13,7 @@ export class UpdateProgressDialogComponent implements OnInit {
|
||||
updateInterval = 250;
|
||||
errored = false;
|
||||
|
||||
constructor(private postsService: PostsService, private snackBar: MatSnackBar) { }
|
||||
constructor(private postsService: PostsService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getUpdateProgress();
|
||||
@@ -28,16 +27,9 @@ export class UpdateProgressDialogComponent implements OnInit {
|
||||
if (res) {
|
||||
this.updateStatus = res;
|
||||
if (this.updateStatus && this.updateStatus['error']) {
|
||||
this.openSnackBar('Update failed. Check logs for more details.');
|
||||
this.postsService.openSnackBar($localize`Update failed. Check logs for more details.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public openSnackBar(message: string, action: string = '') {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -47,6 +47,14 @@
|
||||
|
||||
<mat-divider style="margin-bottom: 16px;"></mat-divider>
|
||||
|
||||
<div *ngIf="!new_file.isAudio" class="info-item">
|
||||
<div class="info-item-label"><strong><ng-container i18n="Video resolution property">Resolution:</ng-container> </strong></div>
|
||||
<div class="info-item-value">{{new_file.height ? new_file.height + 'p' : 'N/A'}}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-item-label"><strong><ng-container i18n="Video audio bitrate property">Audio bitrate:</ng-container> </strong></div>
|
||||
<div class="info-item-value">{{new_file.abr ? new_file.abr + ' Kbps' : 'N/A'}}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-item-label"><strong><ng-container i18n="Video file size property">File size:</ng-container> </strong></div>
|
||||
<div class="info-item-value">{{new_file.size ? filesize(new_file.size) : 'N/A'}}</div>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</div>
|
||||
</form>
|
||||
<br/>
|
||||
<mat-checkbox [disabled]="current_download" (change)="videoModeChanged($event)" [(ngModel)]="audioOnly" style="float: left; margin-top: -12px">
|
||||
<mat-checkbox [disabled]="autoplay && current_download" (change)="videoModeChanged($event)" [(ngModel)]="audioOnly" style="float: left; margin-top: -12px">
|
||||
<ng-container i18n="Only Audio checkbox">
|
||||
Only Audio
|
||||
</ng-container>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, OnInit, ElementRef, ViewChild, ViewChildren, QueryList } from '@angular/core';
|
||||
import {PostsService} from '../posts.services';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import {FormControl, Validators} from '@angular/forms';
|
||||
import {UntypedFormControl, Validators} from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { saveAs } from 'file-saver';
|
||||
@@ -10,12 +10,7 @@ import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { ArgModifierDialogComponent } from 'app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component';
|
||||
import { RecentVideosComponent } from 'app/components/recent-videos/recent-videos.component';
|
||||
import { Download, FileType } from 'api-types';
|
||||
|
||||
export let audioFilesMouseHovering = false;
|
||||
export let videoFilesMouseHovering = false;
|
||||
export let audioFilesOpened = false;
|
||||
export let videoFilesOpened = false;
|
||||
import { DatabaseFile, Download, FileType, Playlist } from 'api-types';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -55,8 +50,6 @@ export class MainComponent implements OnInit {
|
||||
allowQualitySelect = false;
|
||||
downloadOnlyMode = false;
|
||||
allowAutoplay = false;
|
||||
audioFolderPath;
|
||||
videoFolderPath;
|
||||
use_youtubedl_archive = false;
|
||||
globalCustomArgs = null;
|
||||
allowAdvancedDownload = false;
|
||||
@@ -74,16 +67,13 @@ export class MainComponent implements OnInit {
|
||||
results_showing = true;
|
||||
results = [];
|
||||
|
||||
mp3s: any[] = [];
|
||||
mp4s: any[] = [];
|
||||
playlists = {'audio': [], 'video': []};
|
||||
playlist_thumbnails = {};
|
||||
downloading_content = {'audio': {}, 'video': {}};
|
||||
downloads: Download[] = [];
|
||||
download_uids: string[] = [];
|
||||
current_download: Download = null;
|
||||
|
||||
urlForm = new FormControl('', [Validators.required]);
|
||||
urlForm = new UntypedFormControl('', [Validators.required]);
|
||||
|
||||
qualityOptions = {
|
||||
'video': [
|
||||
@@ -206,8 +196,6 @@ export class MainComponent implements OnInit {
|
||||
&& this.postsService.hasPermission('filemanager');
|
||||
this.downloadOnlyMode = this.postsService.config['Extra']['download_only_mode'];
|
||||
this.allowAutoplay = this.postsService.config['Extra']['allow_autoplay'];
|
||||
this.audioFolderPath = this.postsService.config['Downloader']['path-audio'];
|
||||
this.videoFolderPath = this.postsService.config['Downloader']['path-video'];
|
||||
this.use_youtubedl_archive = this.postsService.config['Downloader']['use_youtubedl_archive'];
|
||||
this.globalCustomArgs = this.postsService.config['Downloader']['custom_args'];
|
||||
this.youtubeSearchEnabled = this.postsService.config['API'] && this.postsService.config['API']['use_youtube_API'] &&
|
||||
@@ -314,20 +302,20 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
|
||||
// download helpers
|
||||
downloadHelper(container, type: string, is_playlist = false, force_view = false, navigate_mode = false): void {
|
||||
downloadHelper(container: DatabaseFile | Playlist, type: string, is_playlist = false, force_view = false, navigate_mode = false): void {
|
||||
this.downloadingfile = false;
|
||||
if (!this.autoplay && !this.downloadOnlyMode && !navigate_mode) {
|
||||
// do nothing
|
||||
this.reloadRecentVideos();
|
||||
this.reloadRecentVideos(is_playlist);
|
||||
} else {
|
||||
// if download only mode, just download the file. no redirect
|
||||
if (force_view === false && this.downloadOnlyMode && !this.iOS) {
|
||||
if (is_playlist) {
|
||||
this.downloadPlaylist(container['uid']);
|
||||
} else {
|
||||
this.downloadFileFromServer(container, type);
|
||||
this.downloadFileFromServer(container as DatabaseFile, type);
|
||||
}
|
||||
this.reloadRecentVideos();
|
||||
this.reloadRecentVideos(is_playlist);
|
||||
} else {
|
||||
localStorage.setItem('player_navigator', this.router.url.split(';')[0]);
|
||||
if (is_playlist) {
|
||||
@@ -396,7 +384,7 @@ export class MainComponent implements OnInit {
|
||||
}, () => { // can't access server
|
||||
this.downloadingfile = false;
|
||||
this.current_download = null;
|
||||
this.postsService.openSnackBar('Download failed!', 'OK.');
|
||||
this.postsService.openSnackBar($localize`Download failed!`, 'OK.');
|
||||
});
|
||||
|
||||
if (!this.autoplay && urls.length === 1) {
|
||||
@@ -444,7 +432,7 @@ export class MainComponent implements OnInit {
|
||||
return null;
|
||||
}
|
||||
|
||||
getDownloadByUID(uid: string) {
|
||||
getDownloadByUID(uid: string): Download {
|
||||
const index = this.downloads.findIndex(download => download.uid === uid);
|
||||
if (index !== -1) {
|
||||
return this.downloads[index];
|
||||
@@ -453,7 +441,7 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
removeDownloadFromCurrentDownloads(download_to_remove): boolean {
|
||||
removeDownloadFromCurrentDownloads(download_to_remove: Download): boolean {
|
||||
if (this.current_download === download_to_remove) {
|
||||
this.current_download = null;
|
||||
}
|
||||
@@ -466,11 +454,9 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
downloadFileFromServer(file, type: string): void {
|
||||
downloadFileFromServer(file: DatabaseFile, type: string): void {
|
||||
const ext = type === 'audio' ? 'mp3' : 'mp4'
|
||||
this.downloading_content[type][file.id] = true;
|
||||
this.postsService.downloadFileFromServer(file.uid).subscribe(res => {
|
||||
this.downloading_content[type][file.id] = false;
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, decodeURIComponent(file.id) + `.${ext}`);
|
||||
|
||||
@@ -481,9 +467,8 @@ export class MainComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
downloadPlaylist(playlist): void {
|
||||
downloadPlaylist(playlist: Playlist): void {
|
||||
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');
|
||||
});
|
||||
@@ -603,11 +588,11 @@ export class MainComponent implements OnInit {
|
||||
if (simulated_args) {
|
||||
// hide password if needed
|
||||
const passwordIndex = simulated_args.indexOf('--password');
|
||||
console.log(passwordIndex);
|
||||
if (passwordIndex !== -1 && passwordIndex !== simulated_args.length - 1) {
|
||||
simulated_args[passwordIndex + 1] = simulated_args[passwordIndex + 1].replace(/./g, '*');
|
||||
}
|
||||
this.simulatedOutput = `youtube-dl ${this.url} ${simulated_args.join(' ')}`;
|
||||
const downloader = this.postsService.config.Advanced.default_downloader;
|
||||
this.simulatedOutput = `${downloader} ${this.url} ${simulated_args.join(' ')}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -780,13 +765,14 @@ export class MainComponent implements OnInit {
|
||||
|
||||
if (this.current_download['finished'] && !this.current_download['error']) {
|
||||
const container = this.current_download['container'];
|
||||
const is_playlist = this.current_download['file_uids'].length > 1;
|
||||
this.downloadHelper(container, this.current_download['type'], is_playlist, false);
|
||||
this.current_download = null;
|
||||
const is_playlist = this.current_download['file_uids'].length > 1;
|
||||
const type = this.current_download['type'];
|
||||
this.current_download = null;
|
||||
this.downloadHelper(container, type, is_playlist, false);
|
||||
} else if (this.current_download['finished'] && this.current_download['error']) {
|
||||
this.downloadingfile = false;
|
||||
this.current_download = null;
|
||||
this.postsService.openSnackBar('Download failed!', 'OK.');
|
||||
this.postsService.openSnackBar($localize`Download failed!`, 'OK.');
|
||||
}
|
||||
} else {
|
||||
// console.log('failed to get new download');
|
||||
@@ -794,8 +780,9 @@ export class MainComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
reloadRecentVideos(): void {
|
||||
reloadRecentVideos(is_playlist = false): void {
|
||||
this.postsService.files_changed.next(true);
|
||||
if (is_playlist) this.postsService.playlists_changed.next(true);
|
||||
}
|
||||
|
||||
getURLArray(url_str: string): Array<string> {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { VgApiService } from '@videogular/ngx-videogular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { ShareMediaDialogComponent } from '../dialogs/share-media-dialog/share-media-dialog.component';
|
||||
import { FileType } from '../../api-types';
|
||||
@@ -109,7 +108,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
constructor(public postsService: PostsService, private route: ActivatedRoute, private dialog: MatDialog, private router: Router,
|
||||
public snackBar: MatSnackBar, private cdr: ChangeDetectorRef) {
|
||||
private cdr: ChangeDetectorRef) {
|
||||
|
||||
}
|
||||
processConfig(): void {
|
||||
@@ -147,7 +146,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.postsService.getFile(this.uid, this.uuid).subscribe(res => {
|
||||
this.db_file = res['file'];
|
||||
if (!this.db_file) {
|
||||
this.postsService.openSnackBar('Failed to get file information from the server.', 'Dismiss');
|
||||
this.postsService.openSnackBar($localize`Failed to get file information from the server.`, 'Dismiss');
|
||||
return;
|
||||
}
|
||||
this.postsService.incrementViewCount(this.db_file['uid'], null, this.uuid).subscribe(() => undefined, err => {
|
||||
@@ -169,6 +168,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.uids = this.subscription.videos.map(video => video['uid']);
|
||||
this.parseFileNames();
|
||||
}, () => {
|
||||
// TODO: Make translatable
|
||||
this.postsService.openSnackBar(`Failed to find subscription ${this.sub_id}`, 'Dismiss');
|
||||
});
|
||||
}
|
||||
@@ -183,10 +183,10 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.show_player = true;
|
||||
this.parseFileNames();
|
||||
} else {
|
||||
this.postsService.openSnackBar('Failed to load playlist!', '');
|
||||
this.postsService.openSnackBar($localize`Failed to load playlist!`);
|
||||
}
|
||||
}, () => {
|
||||
this.postsService.openSnackBar('Failed to load playlist!', '');
|
||||
this.postsService.openSnackBar($localize`Failed to load playlist!`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -351,7 +351,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
width: '60vw'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(res => {
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
if (!this.playlist_id) {
|
||||
this.getFile();
|
||||
} else {
|
||||
@@ -361,12 +361,16 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
openFileInfoDialog(): void {
|
||||
this.dialog.open(VideoInfoDialogComponent, {
|
||||
const dialogRef = this.dialog.open(VideoInfoDialogComponent, {
|
||||
data: {
|
||||
file: this.db_file,
|
||||
},
|
||||
minWidth: '50vw'
|
||||
})
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.db_file = dialogRef.componentInstance.file;
|
||||
});
|
||||
}
|
||||
|
||||
setPlaybackTimestamp(time: number): void {
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/observable/throw';
|
||||
import { THEMES_CONFIG } from '../themes';
|
||||
import { Router, CanActivate } from '@angular/router';
|
||||
import { Router, CanActivate, ActivatedRouteSnapshot } from '@angular/router';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
@@ -97,7 +97,11 @@ import {
|
||||
Schedule,
|
||||
ClearDownloadsRequest,
|
||||
Category,
|
||||
UpdateFileRequest
|
||||
UpdateFileRequest,
|
||||
Sort,
|
||||
FileTypeFilter,
|
||||
GetAllFilesRequest,
|
||||
GetAllTasksResponse
|
||||
} from '../api-types';
|
||||
import { isoLangs } from './settings/locales_list';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
@@ -225,12 +229,15 @@ export class PostsService implements CanActivate {
|
||||
}
|
||||
|
||||
}
|
||||
canActivate(route, state): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
resolve(true);
|
||||
})
|
||||
console.log(route);
|
||||
throw new Error('Method not implemented.');
|
||||
canActivate(route: ActivatedRouteSnapshot, state): Promise<boolean> {
|
||||
const PATH_TO_REQUIRED_PERM = {
|
||||
settings: 'settings',
|
||||
subscriptions: 'subscriptions',
|
||||
downloads: 'downloads_manager',
|
||||
tasks: 'tasks_manager'
|
||||
}
|
||||
const required_perm = PATH_TO_REQUIRED_PERM[route.routeConfig.path];
|
||||
return required_perm ? this.hasPermission(required_perm) : true;
|
||||
}
|
||||
|
||||
setTheme(theme) {
|
||||
@@ -355,8 +362,9 @@ export class PostsService implements CanActivate {
|
||||
return this.http.post<GetFileResponse>(this.path + 'getFile', body, this.httpOptions);
|
||||
}
|
||||
|
||||
getAllFiles(sort, range, text_search, file_type_filter) {
|
||||
return this.http.post<GetAllFilesResponse>(this.path + 'getAllFiles', {sort: sort, range: range, text_search: text_search, file_type_filter: file_type_filter}, this.httpOptions);
|
||||
getAllFiles(sort: Sort = null, range: number[] = null, text_search: string = null, file_type_filter: FileTypeFilter = FileTypeFilter.BOTH, sub_id: string = null) {
|
||||
const body: GetAllFilesRequest = {sort: sort, range: range, text_search: text_search, file_type_filter: file_type_filter, sub_id: sub_id};
|
||||
return this.http.post<GetAllFilesResponse>(this.path + 'getAllFiles', body, this.httpOptions);
|
||||
}
|
||||
|
||||
updateFile(uid: string, change_obj: Object) {
|
||||
@@ -443,10 +451,9 @@ export class PostsService implements CanActivate {
|
||||
return this.http.post<SuccessObject>(this.path + 'disableSharing', body, this.httpOptions);
|
||||
}
|
||||
|
||||
createPlaylist(playlistName: string, uids: string[], type: FileType, thumbnailURL: string) {
|
||||
createPlaylist(playlistName: string, uids: string[], thumbnailURL: string) {
|
||||
const body: CreatePlaylistRequest = {playlistName: playlistName,
|
||||
uids: uids,
|
||||
type: type,
|
||||
thumbnailURL: thumbnailURL};
|
||||
return this.http.post<CreatePlaylistResponse>(this.path + 'createPlaylist', body, this.httpOptions);
|
||||
}
|
||||
@@ -471,8 +478,8 @@ export class PostsService implements CanActivate {
|
||||
return this.http.post<SuccessObject>(this.path + 'updatePlaylist', body, this.httpOptions);
|
||||
}
|
||||
|
||||
removePlaylist(playlist_id: string, type: FileType) {
|
||||
const body: DeletePlaylistRequest = {playlist_id: playlist_id, type: type};
|
||||
removePlaylist(playlist_id: string) {
|
||||
const body: DeletePlaylistRequest = {playlist_id: playlist_id};
|
||||
return this.http.post<SuccessObject>(this.path + 'deletePlaylist', body, this.httpOptions);
|
||||
}
|
||||
|
||||
@@ -593,7 +600,7 @@ export class PostsService implements CanActivate {
|
||||
}
|
||||
|
||||
getTasks() {
|
||||
return this.http.post<SuccessObject>(this.path + 'getTasks', {}, this.httpOptions);
|
||||
return this.http.post<GetAllTasksResponse>(this.path + 'getTasks', {}, this.httpOptions);
|
||||
}
|
||||
|
||||
resetTasks() {
|
||||
|
||||
@@ -263,11 +263,16 @@
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<mat-form-field class="text-field" color="accent">
|
||||
<input [disabled]="!new_config['API']['use_twitch_API']" [(ngModel)]="new_config['API']['twitch_API_key']" matInput placeholder="Twitch API Key" i18n-placeholder="Twitch API Key setting placeholder" required>
|
||||
<mat-hint><ng-container i18n="Twitch API Key setting hint AKA preamble">Also known as a Client ID.</ng-container> <a target="_blank" href="https://dev.twitch.tv/docs/api/"><ng-container i18n="Twitch API Key setting hint">Generating a key is easy!</ng-container></a></mat-hint>
|
||||
<input [disabled]="!new_config['API']['use_twitch_API']" [(ngModel)]="new_config['API']['twitch_client_ID']" matInput placeholder="Twitch Client ID" i18n-placeholder="Twitch Client ID setting placeholder" required>
|
||||
<mat-hint><a target="_blank" href="https://dev.twitch.tv/docs/api/"><ng-container i18n="Twitch Client ID setting hint">Generating an ID/secret is easy!</ng-container></a></mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-4">
|
||||
<div class="col-12 mt-2">
|
||||
<mat-form-field class="text-field" color="accent">
|
||||
<input [disabled]="!new_config['API']['use_twitch_API']" [(ngModel)]="new_config['API']['twitch_client_secret']" matInput placeholder="Twitch Client Secret" i18n-placeholder="Twitch Client Secret setting placeholder" required>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-2">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_sponsorblock_API']" matTooltip="Enables a button to skip ads when viewing supported videos." i18n-matTooltip="SponsorBlock API tooltip"><ng-container i18n="Use SponsorBlock API setting">Use SponsorBlock API</ng-container></mat-checkbox>
|
||||
</div>
|
||||
<div class="col-12 mt-2 mb-3">
|
||||
@@ -323,6 +328,7 @@
|
||||
|
||||
<div class="test-connection-div">
|
||||
<button (click)="testConnectionString(new_config['Database']['mongodb_connection_string'])" [disabled]="testing_connection_string" mat-flat-button color="accent"><ng-container i18n="Test connection string button">Test connection string</ng-container></button>
|
||||
<mat-spinner class="test-connection-spinner" style="margin-left: 10px" *ngIf="testing_connection_string" [diameter]="25"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<div class="transfer-db-div">
|
||||
|
||||
@@ -105,4 +105,11 @@
|
||||
.action-buttons {
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
}
|
||||
|
||||
.test-connection-spinner {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { moveItemInArray, CdkDragDrop } from '@angular/cdk/drag-drop';
|
||||
import { InputDialogComponent } from 'app/input-dialog/input-dialog.component';
|
||||
import { EditCategoryDialogComponent } from 'app/dialogs/edit-category-dialog/edit-category-dialog.component';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Category } from 'api-types';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
@@ -62,7 +63,7 @@ export class SettingsComponent implements OnInit {
|
||||
Object.keys(this.INDEX_TO_TAB).forEach(key => { this.TAB_TO_INDEX[this.INDEX_TO_TAB[key]] = key; });
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
ngOnInit(): void {
|
||||
if (this.postsService.initialized) {
|
||||
this.getConfig();
|
||||
this.getDBInfo();
|
||||
@@ -90,16 +91,16 @@ export class SettingsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
getConfig(): void {
|
||||
this.initial_config = this.postsService.config;
|
||||
this.new_config = JSON.parse(JSON.stringify(this.initial_config));
|
||||
}
|
||||
|
||||
settingsSame() {
|
||||
settingsSame(): boolean {
|
||||
return JSON.stringify(this.new_config) === JSON.stringify(this.initial_config);
|
||||
}
|
||||
|
||||
saveSettings() {
|
||||
saveSettings(): void {
|
||||
const settingsToSave = {'YoutubeDLMaterial': this.new_config};
|
||||
this.postsService.setConfig(settingsToSave).subscribe(res => {
|
||||
if (res['success']) {
|
||||
@@ -111,31 +112,31 @@ export class SettingsComponent implements OnInit {
|
||||
this.initial_config = JSON.parse(JSON.stringify(this.new_config));
|
||||
this.postsService.reload_config.next(true);
|
||||
}
|
||||
}, err => {
|
||||
}, () => {
|
||||
console.error('Failed to save config!');
|
||||
})
|
||||
}
|
||||
|
||||
cancelSettings() {
|
||||
cancelSettings(): void {
|
||||
this.new_config = JSON.parse(JSON.stringify(this.initial_config));
|
||||
}
|
||||
|
||||
tabChanged(event) {
|
||||
tabChanged(event): void {
|
||||
const index = event['index'];
|
||||
this.router.navigate(['/settings', {tab: this.INDEX_TO_TAB[index]}]);
|
||||
}
|
||||
|
||||
dropCategory(event: CdkDragDrop<string[]>) {
|
||||
dropCategory(event: CdkDragDrop<string[]>): void {
|
||||
moveItemInArray(this.postsService.categories, event.previousIndex, event.currentIndex);
|
||||
this.postsService.updateCategories(this.postsService.categories).subscribe(res => {
|
||||
|
||||
}, err => {
|
||||
this.postsService.openSnackBar('Failed to update categories!');
|
||||
}, () => {
|
||||
this.postsService.openSnackBar($localize`Failed to update categories!`);
|
||||
});
|
||||
}
|
||||
|
||||
openAddCategoryDialog() {
|
||||
const done = new EventEmitter<any>();
|
||||
openAddCategoryDialog(): void {
|
||||
const done = new EventEmitter<boolean>();
|
||||
const dialogRef = this.dialog.open(InputDialogComponent, {
|
||||
width: '300px',
|
||||
data: {
|
||||
@@ -162,7 +163,7 @@ export class SettingsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
deleteCategory(category) {
|
||||
deleteCategory(category: Category): void {
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
data: {
|
||||
dialogTitle: 'Delete category',
|
||||
@@ -175,17 +176,18 @@ export class SettingsComponent implements OnInit {
|
||||
if (confirmed) {
|
||||
this.postsService.deleteCategory(category['uid']).subscribe(res => {
|
||||
if (res['success']) {
|
||||
// TODO: Make translatable
|
||||
this.postsService.openSnackBar(`Successfully deleted ${category['name']}!`);
|
||||
this.postsService.reloadCategories();
|
||||
}
|
||||
}, err => {
|
||||
}, () => {
|
||||
this.postsService.openSnackBar(`Failed to delete ${category['name']}!`);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openEditCategoryDialog(category) {
|
||||
openEditCategoryDialog(category: Category): void {
|
||||
this.dialog.open(EditCategoryDialogComponent, {
|
||||
data: {
|
||||
category: category
|
||||
@@ -193,7 +195,7 @@ export class SettingsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
generateAPIKey() {
|
||||
generateAPIKey(): void {
|
||||
this.postsService.generateNewAPIKey().subscribe(res => {
|
||||
if (res['new_api_key']) {
|
||||
this.initial_config.API.API_key = res['new_api_key'];
|
||||
@@ -202,16 +204,16 @@ export class SettingsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
localeSelectChanged(new_val) {
|
||||
localeSelectChanged(new_val: string): void {
|
||||
localStorage.setItem('locale', new_val);
|
||||
this.openSnackBar('Language successfully changed! Reload to update the page.')
|
||||
this.postsService.openSnackBar($localize`Language successfully changed! Reload to update the page.`)
|
||||
}
|
||||
|
||||
generateBookmarklet() {
|
||||
generateBookmarklet(): void {
|
||||
this.bookmarksite('YTDL-Material', this.generated_bookmarklet_code);
|
||||
}
|
||||
|
||||
generateBookmarkletCode() {
|
||||
generateBookmarkletCode(): string {
|
||||
const currentURL = window.location.href.split('#')[0];
|
||||
const homePageWithArgsURL = currentURL + '#/home;url=';
|
||||
const audioOnly = this.bookmarkletAudioOnly;
|
||||
@@ -226,13 +228,13 @@ export class SettingsComponent implements OnInit {
|
||||
}
|
||||
|
||||
// not currently functioning on most platforms. hence not in use
|
||||
bookmarksite(title, url) {
|
||||
bookmarksite(title: string, url: string): void {
|
||||
// Internet Explorer
|
||||
if (document.all) {
|
||||
window['external']['AddFavorite'](url, title);
|
||||
} else if (window['chrome']) {
|
||||
// Google Chrome
|
||||
this.openSnackBar('Chrome users must drag the \'Alternate URL\' link to your bookmarks.');
|
||||
this.postsService.openSnackBar($localize`Chrome users must drag the 'Alternate URL' link to your bookmarks.`);
|
||||
} else if (window['sidebar']) {
|
||||
// Firefox
|
||||
window['sidebar'].addPanel(title, url, '');
|
||||
@@ -246,7 +248,7 @@ export class SettingsComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
openArgsModifierDialog() {
|
||||
openArgsModifierDialog(): void {
|
||||
const dialogRef = this.dialog.open(ArgModifierDialogComponent, {
|
||||
data: {
|
||||
initial_args: this.new_config['Downloader']['custom_args']
|
||||
@@ -259,20 +261,20 @@ export class SettingsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
getLatestGithubRelease() {
|
||||
getLatestGithubRelease(): void {
|
||||
this.postsService.getLatestGithubRelease().subscribe(res => {
|
||||
this.latestGithubRelease = res;
|
||||
});
|
||||
}
|
||||
|
||||
openCookiesUploaderDialog() {
|
||||
openCookiesUploaderDialog(): void {
|
||||
this.dialog.open(CookiesUploaderDialogComponent, {
|
||||
width: '65vw'
|
||||
});
|
||||
}
|
||||
|
||||
killAllDownloads() {
|
||||
const done = new EventEmitter<any>();
|
||||
killAllDownloads(): void {
|
||||
const done = new EventEmitter<boolean>();
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
data: {
|
||||
dialogTitle: 'Kill downloads',
|
||||
@@ -287,34 +289,34 @@ export class SettingsComponent implements OnInit {
|
||||
this.postsService.killAllDownloads().subscribe(res => {
|
||||
if (res['success']) {
|
||||
dialogRef.close();
|
||||
this.postsService.openSnackBar('Successfully killed all downloads!');
|
||||
this.postsService.openSnackBar($localize`Successfully killed all downloads!`);
|
||||
} else {
|
||||
dialogRef.close();
|
||||
this.postsService.openSnackBar('Failed to kill all downloads! Check logs for details.');
|
||||
this.postsService.openSnackBar($localize`Failed to kill all downloads! Check logs for details.`);
|
||||
}
|
||||
}, err => {
|
||||
}, () => {
|
||||
dialogRef.close();
|
||||
this.postsService.openSnackBar('Failed to kill all downloads! Check logs for details.');
|
||||
this.postsService.openSnackBar($localize`Failed to kill all downloads! Check logs for details.`);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
restartServer() {
|
||||
this.postsService.restartServer().subscribe(res => {
|
||||
this.postsService.openSnackBar('Restarting!');
|
||||
}, err => {
|
||||
this.postsService.openSnackBar('Failed to restart the server.');
|
||||
restartServer(): void {
|
||||
this.postsService.restartServer().subscribe(() => {
|
||||
this.postsService.openSnackBar($localize`Restarting!`);
|
||||
}, () => {
|
||||
this.postsService.openSnackBar($localize`Failed to restart the server.`);
|
||||
});
|
||||
}
|
||||
|
||||
getDBInfo() {
|
||||
getDBInfo(): void {
|
||||
this.postsService.getDBInfo().subscribe(res => {
|
||||
this.db_info = res['db_info'];
|
||||
});
|
||||
}
|
||||
|
||||
transferDB() {
|
||||
transferDB(): void {
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
data: {
|
||||
dialogTitle: 'Transfer DB',
|
||||
@@ -329,44 +331,36 @@ export class SettingsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
_transferDB() {
|
||||
_transferDB(): void {
|
||||
this.db_transferring = true;
|
||||
this.postsService.transferDB(this.db_info['using_local_db']).subscribe(res => {
|
||||
this.db_transferring = false;
|
||||
const success = res['success'];
|
||||
if (success) {
|
||||
this.openSnackBar('Successfully transfered DB! Reloading info...');
|
||||
this.postsService.openSnackBar($localize`Successfully transfered DB! Reloading info...`);
|
||||
this.getDBInfo();
|
||||
} else {
|
||||
this.openSnackBar('Failed to transfer DB -- transfer was aborted. Error: ' + res['error']);
|
||||
this.postsService.openSnackBar($localize`Failed to transfer DB -- transfer was aborted. Error: ` + res['error']);
|
||||
}
|
||||
}, err => {
|
||||
this.db_transferring = false;
|
||||
this.openSnackBar('Failed to transfer DB -- API call failed. See browser logs for details.');
|
||||
this.postsService.openSnackBar($localize`Failed to transfer DB -- API call failed. See browser logs for details.`);
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
testConnectionString(connection_string) {
|
||||
testConnectionString(connection_string: string): void {
|
||||
this.testing_connection_string = true;
|
||||
this.postsService.testConnectionString(connection_string).subscribe(res => {
|
||||
this.testing_connection_string = false;
|
||||
if (res['success']) {
|
||||
this.postsService.openSnackBar('Connection successful!');
|
||||
this.postsService.openSnackBar($localize`Connection successful!`);
|
||||
} else {
|
||||
this.postsService.openSnackBar('Connection failed! Error: ' + res['error']);
|
||||
this.postsService.openSnackBar($localize`Connection failed! Error: ` + res['error']);
|
||||
}
|
||||
}, err => {
|
||||
}, () => {
|
||||
this.testing_connection_string = false;
|
||||
this.postsService.openSnackBar('Connection failed! Error: Server error. See logs for more info.');
|
||||
this.postsService.openSnackBar($localize`Connection failed! Error: Server error. See logs for more info.`);
|
||||
});
|
||||
}
|
||||
|
||||
// snackbar helper
|
||||
public openSnackBar(message: string, action: string = '') {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<div style="position: relative; width: fit-content;">
|
||||
<div class="duration-time">
|
||||
<ng-container i18n="Video duration label">Length:</ng-container> {{formattedDuration}}
|
||||
</div>
|
||||
<button [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||
<mat-menu #action_menu="matMenu">
|
||||
<button (click)="openSubscriptionInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Subscription video info button">Info</ng-container></button>
|
||||
<button (click)="deleteAndRedownload()" mat-menu-item><mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container></button>
|
||||
<button (click)="deleteForever()" mat-menu-item *ngIf="sub.archive && use_youtubedl_archive"><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete forever subscription video button">Delete forever</ng-container></button>
|
||||
</mat-menu>
|
||||
<mat-card (click)="goToFile()" matRipple class="example-card mat-elevation-z6">
|
||||
<div style="padding:5px">
|
||||
<div *ngIf="!image_errored && file.thumbnailURL" class="img-div">
|
||||
<img class="image" (error)="onImgError($event)" [src]="file.thumbnailURL" alt="Thumbnail">
|
||||
</div>
|
||||
|
||||
<span class="max-two-lines"><strong>{{file.title}}</strong></span>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
@@ -1,76 +0,0 @@
|
||||
.example-card {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
padding: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menuButton {
|
||||
right: 0px;
|
||||
top: -1px;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
|
||||
}
|
||||
|
||||
/* Coerce the <span> icon container away from display:inline */
|
||||
.mat-icon-button .mat-button-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 200px;
|
||||
height: 112.5px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.example-full-width-height {
|
||||
width: 100%;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.centered {
|
||||
margin: 0 auto;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.img-div {
|
||||
max-height: 80px;
|
||||
padding: 0px;
|
||||
margin: 32px 0px 0px -5px;
|
||||
width: calc(100% + 5px + 5px);
|
||||
}
|
||||
|
||||
.max-two-lines {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
max-height: 2.4em;
|
||||
line-height: 1.2em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
bottom: 5px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.duration-time {
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 5px;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
@media (max-width: 576px){
|
||||
|
||||
.example-card {
|
||||
width: 175px !important;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { SubscriptionFileCardComponent } from './subscription-file-card.component';
|
||||
|
||||
describe('SubscriptionFileCardComponent', () => {
|
||||
let component: SubscriptionFileCardComponent;
|
||||
let fixture: ComponentFixture<SubscriptionFileCardComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SubscriptionFileCardComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SubscriptionFileCardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,98 +0,0 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-subscription-file-card',
|
||||
templateUrl: './subscription-file-card.component.html',
|
||||
styleUrls: ['./subscription-file-card.component.scss']
|
||||
})
|
||||
export class SubscriptionFileCardComponent implements OnInit {
|
||||
image_errored = false;
|
||||
image_loaded = false;
|
||||
|
||||
formattedDuration = null;
|
||||
|
||||
@Input() file;
|
||||
@Input() sub;
|
||||
@Input() use_youtubedl_archive = false;
|
||||
|
||||
@Output() goToFileEmit = new EventEmitter<any>();
|
||||
@Output() reloadSubscription = new EventEmitter<boolean>();
|
||||
|
||||
constructor(private snackBar: MatSnackBar, private postsService: PostsService, private dialog: MatDialog) {}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.file.duration) {
|
||||
this.formattedDuration = fancyTimeFormat(this.file.duration);
|
||||
}
|
||||
}
|
||||
|
||||
onImgError(event) {
|
||||
this.image_errored = true;
|
||||
}
|
||||
|
||||
imageLoaded(loaded) {
|
||||
this.image_loaded = true;
|
||||
}
|
||||
|
||||
goToFile() {
|
||||
const emit_obj = {
|
||||
uid: this.file.uid,
|
||||
url: this.file.requested_formats ? this.file.requested_formats[0].url : this.file.url
|
||||
}
|
||||
this.goToFileEmit.emit(emit_obj);
|
||||
}
|
||||
|
||||
openSubscriptionInfoDialog() {
|
||||
const dialogRef = this.dialog.open(VideoInfoDialogComponent, {
|
||||
data: {
|
||||
file: this.file,
|
||||
},
|
||||
minWidth: '50vw'
|
||||
});
|
||||
}
|
||||
|
||||
deleteAndRedownload() {
|
||||
this.postsService.deleteSubscriptionFile(this.sub, this.file.id, false, this.file.uid).subscribe(res => {
|
||||
this.reloadSubscription.emit(true);
|
||||
this.openSnackBar(`Successfully deleted file: '${this.file.id}'`, 'Dismiss.');
|
||||
});
|
||||
}
|
||||
|
||||
deleteForever() {
|
||||
this.postsService.deleteSubscriptionFile(this.sub, this.file.id, true, this.file.uid).subscribe(res => {
|
||||
this.reloadSubscription.emit(true);
|
||||
this.openSnackBar(`Successfully deleted file: '${this.file.id}'`, 'Dismiss.');
|
||||
});
|
||||
}
|
||||
|
||||
public openSnackBar(message: string, action: string) {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function fancyTimeFormat(time) {
|
||||
// Hours, minutes and seconds
|
||||
const hrs = ~~(time / 3600);
|
||||
const mins = ~~((time % 3600) / 60);
|
||||
const secs = ~~time % 60;
|
||||
|
||||
// Output like "1:01" or "4:03:59" or "123:03:59"
|
||||
let ret = '';
|
||||
|
||||
if (hrs > 0) {
|
||||
ret += '' + hrs + ':' + (mins < 10 ? '0' : '');
|
||||
}
|
||||
|
||||
ret += '' + mins + ':' + (secs < 10 ? '0' : '');
|
||||
ret += '' + secs;
|
||||
return ret;
|
||||
}
|
||||
@@ -10,38 +10,7 @@
|
||||
<br/>
|
||||
|
||||
<div *ngIf="subscription">
|
||||
<div class="flex-grid">
|
||||
<div class="filter-select-parent">
|
||||
<div style="display: inline-block;">
|
||||
<mat-select style="width: 110px;" [(ngModel)]="this.filterProperty" (selectionChange)="filterOptionChanged($event.value)">
|
||||
<mat-option *ngFor="let filterOption of filterProperties | keyvalue" [value]="filterOption.value">
|
||||
{{filterOption['value']['label']}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</div>
|
||||
<div style="display: inline-block;">
|
||||
<button (click)="toggleModeChange()" mat-icon-button><mat-icon>{{descendingMode ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
</div>
|
||||
<div class="col">
|
||||
<h4 i18n="Subscription videos title" style="text-align: center; margin-bottom: 20px;">Videos</h4>
|
||||
</div>
|
||||
<div style="top: -12px;" class="col">
|
||||
<mat-form-field [ngClass]="searchIsFocused ? 'search-bar-focused' : 'search-bar-unfocused'" class="search-bar" color="accent">
|
||||
<input (focus)="searchIsFocused = true" (blur)="searchIsFocused = false" class="search-input" type="text" placeholder="Search" i18n-placeholder="Subscription videos search placeholder" [(ngModel)]="search_text" (ngModelChange)="onSearchInputChanged($event)" matInput>
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div *ngFor="let file of filtered_files" class="col-6 col-lg-4 mb-2 mt-2 sub-file-col">
|
||||
<app-subscription-file-card (reloadSubscription)="getSubscription()" (goToFileEmit)="goToFile($event)" [file]="file" [sub]="subscription" [use_youtubedl_archive]="use_youtubedl_archive"></app-subscription-file-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<app-recent-videos [sub_id]="subscription.id" [usePaginator]="false"></app-recent-videos>
|
||||
</div>
|
||||
<button class="edit-button" color="primary" (click)="editSubscription()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">edit</mat-icon></button>
|
||||
<button class="watch-button" color="primary" (click)="watchSubscription()" mat-fab><mat-icon class="save-icon">video_library</mat-icon></button>
|
||||
|
||||
@@ -100,22 +100,12 @@ export class SubscriptionComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
getConfig(): void {
|
||||
this.use_youtubedl_archive = this.postsService.config['Downloader']['use_youtubedl_archive'];
|
||||
}
|
||||
|
||||
goToFile(emit_obj) {
|
||||
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', {uid: uid, url: url}]);
|
||||
} else {
|
||||
this.router.navigate(['/player', {uid: uid}]);
|
||||
}
|
||||
}
|
||||
|
||||
onSearchInputChanged(newvalue) {
|
||||
onSearchInputChanged(newvalue: string): void {
|
||||
if (newvalue.length > 0) {
|
||||
this.search_mode = true;
|
||||
this.filterFiles(newvalue);
|
||||
@@ -129,7 +119,7 @@ export class SubscriptionComponent implements OnInit, OnDestroy {
|
||||
this.filtered_files = this.files.filter(option => option.id.toLowerCase().includes(filterValue));
|
||||
}
|
||||
|
||||
filterByProperty(prop) {
|
||||
filterByProperty(prop: string): void {
|
||||
if (this.descendingMode) {
|
||||
this.filtered_files = this.filtered_files.sort((a, b) => (a[prop] > b[prop] ? -1 : 1));
|
||||
} else {
|
||||
@@ -142,17 +132,12 @@ export class SubscriptionComponent implements OnInit, OnDestroy {
|
||||
localStorage.setItem('filter_property', value['key']);
|
||||
}
|
||||
|
||||
toggleModeChange() {
|
||||
toggleModeChange(): void {
|
||||
this.descendingMode = !this.descendingMode;
|
||||
this.filterByProperty(this.filterProperty['property']);
|
||||
}
|
||||
|
||||
downloadContent() {
|
||||
const fileNames = [];
|
||||
for (let i = 0; i < this.files.length; i++) {
|
||||
fileNames.push(this.files[i].path);
|
||||
}
|
||||
|
||||
downloadContent(): void {
|
||||
this.downloading = true;
|
||||
this.postsService.downloadSubFromServer(this.subscription.id).subscribe(res => {
|
||||
this.downloading = false;
|
||||
@@ -164,7 +149,7 @@ export class SubscriptionComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
editSubscription() {
|
||||
editSubscription(): void {
|
||||
this.dialog.open(EditSubscriptionDialogComponent, {
|
||||
data: {
|
||||
sub: this.postsService.getSubscriptionByID(this.subscription.id)
|
||||
@@ -172,7 +157,7 @@ export class SubscriptionComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
watchSubscription() {
|
||||
watchSubscription(): void {
|
||||
this.router.navigate(['/player', {sub_id: this.subscription.id}])
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3332,7 +3332,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="b1c08387975e6feada407c9b5f5f564261b8192b" datatype="html">
|
||||
<source>Database information could not be retrieved. Check the server logs for more information.</source>
|
||||
<target state="translated">Impossibile recuperare le informazioni del database. Controllare i registri del server per ulteriori informazioni.</target>
|
||||
<target state="translated">Impossibile recuperare le informazioni del database. Controllare il registro del server per ulteriori informazioni.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||
<context context-type="linenumber">333</context>
|
||||
@@ -3347,6 +3347,698 @@
|
||||
<context context-type="linenumber">48</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5070854963159885174" datatype="html">
|
||||
<source>Playlist successfully removed.', '</source>
|
||||
<target state="translated">Playlist rimossa con successo.', '</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
|
||||
<context context-type="linenumber">99</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5215119607776782829" datatype="html">
|
||||
<source>Select downloads to clear</source>
|
||||
<target state="translated">Seleziona i download da cancellare</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||
<context context-type="linenumber">132</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2723988842145709249" datatype="html">
|
||||
<source>Errored downloads</source>
|
||||
<target state="translated">Download errati</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||
<context context-type="linenumber">146</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="3961621815065792326" datatype="html">
|
||||
<source>Failed to clear finished downloads!</source>
|
||||
<target state="translated">Cancellazione dei download finiti non riuscita!</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||
<context context-type="linenumber">157</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5823550543348347814" datatype="html">
|
||||
<source>Cleared downloads!</source>
|
||||
<target state="translated">Download cancellati!</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||
<context context-type="linenumber">159</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2293081271355999967" datatype="html">
|
||||
<source>Logs successfully cleared!</source>
|
||||
<target state="translated">Registro cancellato con successo!</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
|
||||
<context context-type="linenumber">75</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4516710756538206828" datatype="html">
|
||||
<source>Failed to clear logs!</source>
|
||||
<target state="translated">Impossibile cancellare il registro!</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
|
||||
<context context-type="linenumber">77</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
|
||||
<context context-type="linenumber">80</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="52e0fa8ada52c3f29774a4508582fd98250b9f93" datatype="html">
|
||||
<source>My files</source>
|
||||
<target state="translated">I miei file</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
|
||||
<context context-type="linenumber">20</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">My files title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="6827066f436adfc56a142d5816a8be6113d73b01" datatype="html">
|
||||
<source>No files found.</source>
|
||||
<target state="translated">Nessun file trovato.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
|
||||
<context context-type="linenumber">40</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">No files found</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="5704ec2049d007c5f5fb495a5d8b607e68d58081" datatype="html">
|
||||
<source>Order</source>
|
||||
<target state="translated">Ordina</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
|
||||
<context context-type="linenumber">53</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Order</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="ae9a5141f5c6bd62cee4ce837598ea8b0904e5cf" datatype="html">
|
||||
<source>Select files</source>
|
||||
<target state="translated">Seleziona file</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
|
||||
<context context-type="linenumber">71</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Select files</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="2159130950882492111" datatype="html">
|
||||
<source>Cancel</source>
|
||||
<target state="translated">Annulla</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/confirm-dialog/confirm-dialog.component.ts</context>
|
||||
<context context-type="linenumber">15</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="1709839462010459086" datatype="html">
|
||||
<source>Cookies successfully uploaded!</source>
|
||||
<target state="translated">Cookie caricati con successo!</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.ts</context>
|
||||
<context context-type="linenumber">42</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="3480433876298276350" datatype="html">
|
||||
<source>Database successfully restored!</source>
|
||||
<target state="translated">Database ripristinato con successo!</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/restore-db-dialog/restore-db-dialog.component.ts</context>
|
||||
<context context-type="linenumber">39</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="1946323844380374711" datatype="html">
|
||||
<source>Failed to restore database! See browser console for more info.</source>
|
||||
<target state="translated">Ripristino del database non riuscito! Guarda la console del browser per più informazioni.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/restore-db-dialog/restore-db-dialog.component.ts</context>
|
||||
<context context-type="linenumber">46</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4960870191807928282" datatype="html">
|
||||
<source>Sharing enabled.</source>
|
||||
<target state="translated">Condivisione abilitata.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
|
||||
<context context-type="linenumber">68</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2720327817780634026" datatype="html">
|
||||
<source>Failed to enable sharing.</source>
|
||||
<target state="translated">Abilitazione della condivisione non riuscita.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
|
||||
<context context-type="linenumber">71</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5397815846940616259" datatype="html">
|
||||
<source>You must specify an amount of time</source>
|
||||
<target state="translated">Devi specificare un periodo di tempo</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/subscribe-dialog/subscribe-dialog.component.ts</context>
|
||||
<context context-type="linenumber">79</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="3544790314111256717" datatype="html">
|
||||
<source>ERROR:</source>
|
||||
<target state="translated">ERRORE:</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/subscribe-dialog/subscribe-dialog.component.ts</context>
|
||||
<context context-type="linenumber">95</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="f4003b626fcbf3a871778d4dba166e109d02f87c" datatype="html">
|
||||
<source>Thumbnail URL</source>
|
||||
<target state="translated">URL della miniatura</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
|
||||
<context context-type="linenumber">29</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Thumbnail URL</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="607de17c2a755f65775881c19e276e7c933bcf94" datatype="html">
|
||||
<source>Category</source>
|
||||
<target state="translated">Categoria</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
|
||||
<context context-type="linenumber">32</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Category</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="3f741a2c015bb728088b630296ca401e823c6af8" datatype="html">
|
||||
<source>View count</source>
|
||||
<target state="translated">Numero di visualizzazioni</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
|
||||
<context context-type="linenumber">42</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">View count</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="989f5aa799ee9672675d68109bff29d1d88ebd49" datatype="html">
|
||||
<source>Local view count</source>
|
||||
<target state="translated">Numero di visualizzazioni locale</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
|
||||
<context context-type="linenumber">45</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Local view count</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="9fa37704969eeebd496a172c5077370f569df3ae" datatype="html">
|
||||
<source>Resolution:</source>
|
||||
<target state="translated">Risoluzione:</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
|
||||
<context context-type="linenumber">51</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Video resolution property</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="9fc54db2830fbbd332b1adebe28e9283069107ef" datatype="html">
|
||||
<source>Audio bitrate:</source>
|
||||
<target state="translated">Bitrate audio:</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
|
||||
<context context-type="linenumber">55</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Video audio bitrate property</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="2734512985872312443" datatype="html">
|
||||
<source>Failed to get file information from the server.</source>
|
||||
<target state="translated">Impossibile ottenere le informazioni del file dal server.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/player/player.component.ts</context>
|
||||
<context context-type="linenumber">149</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5d78fe9ba69a8710613d3f7c35b22e9c8226e4dc" datatype="html">
|
||||
<source>Twitch Client ID</source>
|
||||
<target state="translated">ID Client Twitch</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||
<context context-type="linenumber">266</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Twitch Client ID setting placeholder</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="9208873922277364009" datatype="html">
|
||||
<source>Failed to update categories!</source>
|
||||
<target state="translated">Aggiornamento delle categorie non riuscito!</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||
<context context-type="linenumber">134</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7180231139026789468" datatype="html">
|
||||
<source>Language successfully changed! Reload to update the page.</source>
|
||||
<target state="translated">Lingua cambiata con successo! Ricarica per aggiornare la pagina.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||
<context context-type="linenumber">209</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6123898845299902958" datatype="html">
|
||||
<source>Successfully transfered DB! Reloading info...</source>
|
||||
<target state="translated">DB trasferito con successo! Ricaricando informazioni...</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||
<context context-type="linenumber">340</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5681417617361245213" datatype="html">
|
||||
<source>Failed to transfer DB -- transfer was aborted. Error:</source>
|
||||
<target state="translated">Trasferimento del DB non riuscito -- transferimento interrotto. Errore:</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||
<context context-type="linenumber">343</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6519219215739537829" datatype="html">
|
||||
<source>Connection failed! Error: Server error. See logs for more info.</source>
|
||||
<target state="translated">Connessione non riuscita! Errore: Errore server. Guarda il registro per più informazioni.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||
<context context-type="linenumber">363</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="65918861159071115" datatype="html">
|
||||
<source>Successfully created playlist!', '</source>
|
||||
<target state="translated">Playlist creata con successo!', '</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
|
||||
<context context-type="linenumber">56</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7114033980971410157" datatype="html">
|
||||
<source>Failed to pause download! See server logs for more info.</source>
|
||||
<target state="translated">Interruzione del download non riuscita! Guarda il registro del server per più informazioni.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||
<context context-type="linenumber">170</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||
<context context-type="linenumber">218</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="8348223454028662277" datatype="html">
|
||||
<source>OK.</source>
|
||||
<target state="translated">OK.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
|
||||
<context context-type="linenumber">270</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
|
||||
<context context-type="linenumber">273</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
|
||||
<context context-type="linenumber">276</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="22bac71dbdc1ac62607135994f81cca8094cb251" datatype="html">
|
||||
<source>Upload date</source>
|
||||
<target state="translated">Data caricamento</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
|
||||
<context context-type="linenumber">20</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Upload date</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="1019978815798793544" datatype="html">
|
||||
<source>Failed to retrieve logs!</source>
|
||||
<target state="translated">Recupero dei registro non riuscito!</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
|
||||
<context context-type="linenumber">46</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
|
||||
<context context-type="linenumber">51</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="3481862581074838726" datatype="html">
|
||||
<source>VOD url for this video is not supported. VOD ID must be after "twitch.tv/videos/"</source>
|
||||
<target state="translated">L'url del VOD di questo video non è supportato. L'ID del VOD deve essere dopo "twitch.tv/videos/"</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/twitch-chat/twitch-chat.component.ts</context>
|
||||
<context context-type="linenumber">99</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7241816854520039909" datatype="html">
|
||||
<source>ERROR: failed to create playlist!', '</source>
|
||||
<target state="translated">ERRORE: creazione della playlist non riuscita!', '</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
|
||||
<context context-type="linenumber">58</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="a4a4a5f03d7d0831ccf6774094e66a9507a42b58" datatype="html">
|
||||
<source>Clear downloads</source>
|
||||
<target state="translated">Cancella i download</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/downloads/downloads.component.html</context>
|
||||
<context context-type="linenumber">85</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Clear downloads</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="3299455901271096793" datatype="html">
|
||||
<source>Clear downloads</source>
|
||||
<target state="translated">Cancella i download</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||
<context context-type="linenumber">131</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4050356167294261426" datatype="html">
|
||||
<source>Delete success!</source>
|
||||
<target state="translated">Cancellazione avvenuta con successo!</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
|
||||
<context context-type="linenumber">270</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7405156667148936748" datatype="html">
|
||||
<source>Delete failed!</source>
|
||||
<target state="translated">Cancellazione non riuscita!</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
|
||||
<context context-type="linenumber">273</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
|
||||
<context context-type="linenumber">276</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="8485375438204712002" datatype="html">
|
||||
<source>Finished downloads</source>
|
||||
<target state="translated">Download terminati</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||
<context context-type="linenumber">138</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5223827577229167333" datatype="html">
|
||||
<source>Failed to pause all downloads! See server logs for more info.</source>
|
||||
<target state="translated">Interruzione di tutti i download non riuscita! Guarda il registro del server per più informazioni.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||
<context context-type="linenumber">178</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5801924165267871854" datatype="html">
|
||||
<source>Paused downloads</source>
|
||||
<target state="translated">Download interrotti</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||
<context context-type="linenumber">142</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7157191502004604261" datatype="html">
|
||||
<source>Chat could not be downloaded.</source>
|
||||
<target state="translated">Impossibile scaricare la chat.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/twitch-chat/twitch-chat.component.ts</context>
|
||||
<context context-type="linenumber">110</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="87406377200084623" datatype="html">
|
||||
<source>Logs copied to clipboard!</source>
|
||||
<target state="translated">Registro copiato negli appunti!</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
|
||||
<context context-type="linenumber">56</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5456775416888155476" datatype="html">
|
||||
<source>Failed to resume download! See server logs for more info.</source>
|
||||
<target state="translated">Ripresa del download non riuscita! Guarda il registro del server per più informazioni.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||
<context context-type="linenumber">186</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6268791413935580107" datatype="html">
|
||||
<source>Failed to resume all downloads! See server logs for more info.</source>
|
||||
<target state="translated">Ripresa di tutti i download non riuscita! Guarda il registro del server per più informazioni.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||
<context context-type="linenumber">194</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="571023367671104036" datatype="html">
|
||||
<source>Failed to restart download! See server logs for more info.</source>
|
||||
<target state="translated">Riavvio del download non riuscito! Guarda il registro del server per più informazioni.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||
<context context-type="linenumber">202</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4529487534884306633" datatype="html">
|
||||
<source>Failed to cancel download! See server logs for more info.</source>
|
||||
<target state="translated">Annullamento del download non riuscito! Guarda il registro del server per più informazioni.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
|
||||
<context context-type="linenumber">210</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="c1b7e6d75ff4285c7636c67e5ef259629b81725b" datatype="html">
|
||||
<source>Confirm Password</source>
|
||||
<target state="translated">Conferma password</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/login/login.component.html</context>
|
||||
<context context-type="linenumber">28</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Confirm Password</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="8937901770314883418" datatype="html">
|
||||
<source>Successfully deleted file:</source>
|
||||
<target state="translated">File eliminati con successo:</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
|
||||
<context context-type="linenumber">291</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
|
||||
<context context-type="linenumber">299</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="e58f5716d6c08b6a841eb003c9f9774b5c5d34a9" datatype="html">
|
||||
<source>Delete and don't download again</source>
|
||||
<target state="translated">Elimina e non scaricare di nuovo</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
|
||||
<context context-type="linenumber">37</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
|
||||
<context context-type="linenumber">40</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Delete forever subscription video button</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="9203653061903371757" datatype="html">
|
||||
<source>Playlist updated successfully.</source>
|
||||
<target state="translated">Playlist aggiornata con successo.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/create-playlist/create-playlist.component.ts</context>
|
||||
<context context-type="linenumber">69</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/create-playlist/create-playlist.component.ts</context>
|
||||
<context context-type="linenumber">75</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="880407735794041263" datatype="html">
|
||||
<source>Download failed.</source>
|
||||
<target state="translated">Download non riuscito.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/twitch-chat/twitch-chat.component.ts</context>
|
||||
<context context-type="linenumber">106</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="643438049907907768" datatype="html">
|
||||
<source>Failed to restore database! See logs for more info.</source>
|
||||
<target state="translated">Ripristino del database non riuscito! Guarda il registro del server per più informazioni.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/restore-db-dialog/restore-db-dialog.component.ts</context>
|
||||
<context context-type="linenumber">42</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2859348955905483094" datatype="html">
|
||||
<source>Failed to enable sharing - server error.</source>
|
||||
<target state="translated">Abilitazione della condivisione non riuscita - errore server.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
|
||||
<context context-type="linenumber">74</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="3561468911579213356" datatype="html">
|
||||
<source>Sharing disabled.</source>
|
||||
<target state="translated">Condivisione disabilitata.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
|
||||
<context context-type="linenumber">79</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="8692976466689769553" datatype="html">
|
||||
<source>Failed to disable sharing - server error.</source>
|
||||
<target state="translated">Disabilitazione della condivisione non riuscita - errore server.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
|
||||
<context context-type="linenumber">85</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2876893175497409225" datatype="html">
|
||||
<source>Update failed. Check logs for more details.</source>
|
||||
<target state="translated">Aggiornamento non riuscito. Guarda il registro per più dettagli.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/update-progress-dialog/update-progress-dialog.component.ts</context>
|
||||
<context context-type="linenumber">30</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="3317b8688eb2cfabc4021cd7b2926b32f3864ad2" datatype="html">
|
||||
<source>Choose a date</source>
|
||||
<target state="translated">Seleziona una data</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.html</context>
|
||||
<context context-type="linenumber">22</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Choose a date</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="7840375760456214518" datatype="html">
|
||||
<source>Failed to disable sharing.</source>
|
||||
<target state="translated">Disabilitazione della condivisione non riuscita.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
|
||||
<context context-type="linenumber">82</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="f8c7be184fefd6750e4e5d0c7a90e74721c58f8a" datatype="html">
|
||||
<source>Uploader</source>
|
||||
<target state="translated">Caricato da</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
|
||||
<context context-type="linenumber">17</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Uploader</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="d49d5d6786b69d140e20cfddfe29690a19641a88" datatype="html">
|
||||
<source>Thumbnail path</source>
|
||||
<target state="translated">Percorso della miniatura</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
|
||||
<context context-type="linenumber">26</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Thumbnail path</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="8314249599019746316" datatype="html">
|
||||
<source>Download failed!</source>
|
||||
<target state="translated">Download non riuscito!</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/main/main.component.ts</context>
|
||||
<context context-type="linenumber">387</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/main/main.component.ts</context>
|
||||
<context context-type="linenumber">775</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6789263921624845085" datatype="html">
|
||||
<source>Failed to load playlist!</source>
|
||||
<target state="translated">Caricamento della playlist non riuscito!</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/player/player.component.ts</context>
|
||||
<context context-type="linenumber">186</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/player/player.component.ts</context>
|
||||
<context context-type="linenumber">189</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4c9a15ab7fb3dce1002ea7aea4ecada3c1ee12e9" datatype="html">
|
||||
<source>Generating an ID/secret is easy!</source>
|
||||
<target state="translated">Generare un ID/secret è facile!</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||
<context context-type="linenumber">267</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Twitch Client ID setting hint</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="8506540da14d205ea092b4c856e242ed7f500643" datatype="html">
|
||||
<source>Twitch Client Secret</source>
|
||||
<target state="translated">Secret Client Twitch</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||
<context context-type="linenumber">272</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Twitch Client Secret setting placeholder</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="4604336107574138791" datatype="html">
|
||||
<source>Chrome users must drag the 'Alternate URL' link to your bookmarks.</source>
|
||||
<target state="translated">Gli utenti di Chrome devono trascinare il link del 'Alternate URL' nei preferiti.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||
<context context-type="linenumber">237</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4257962986336738751" datatype="html">
|
||||
<source>Successfully killed all downloads!</source>
|
||||
<target state="translated">Uccisi con successo tutti i download!</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||
<context context-type="linenumber">292</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="1942965859829798388" datatype="html">
|
||||
<source>Restarting!</source>
|
||||
<target state="translated">Riavviando!</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||
<context context-type="linenumber">307</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6224607866493148072" datatype="html">
|
||||
<source>Failed to restart the server.</source>
|
||||
<target state="translated">Riavvio del server non riuscito.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||
<context context-type="linenumber">309</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2600933489084742998" datatype="html">
|
||||
<source>Failed to kill all downloads! Check logs for details.</source>
|
||||
<target state="translated">Impossibile uccidere tutti i download! Guarda il registro per più informazioni.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||
<context context-type="linenumber">295</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||
<context context-type="linenumber">299</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="1716030487077666916" datatype="html">
|
||||
<source>Failed to transfer DB -- API call failed. See browser logs for details.</source>
|
||||
<target state="translated">Trasferimento del DB non riuscito -- Errore chiamata API. Guarda il registro del browser per più informazioni.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||
<context context-type="linenumber">347</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6018050954136387828" datatype="html">
|
||||
<source>Connection successful!</source>
|
||||
<target state="translated">Connessione avvenuta con successo!</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||
<context context-type="linenumber">357</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4021495815084152271" datatype="html">
|
||||
<source>Connection failed! Error:</source>
|
||||
<target state="translated">Connessione non riuscita! Errore:</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
|
||||
<context context-type="linenumber">359</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
3241
src/assets/i18n/messages.sv.xlf
Normal file
3241
src/assets/i18n/messages.sv.xlf
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es2019",
|
||||
"target": "es2020",
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"typeRoots": [
|
||||
|
||||
Reference in New Issue
Block a user