diff --git a/Public API v1.yaml b/Public API v1.yaml
index 3e84cee..0650019 100644
--- a/Public API v1.yaml
+++ b/Public API v1.yaml
@@ -111,6 +111,37 @@ paths:
$ref: '#/components/schemas/GetAllFilesResponse'
security:
- Auth query parameter: []
+ /api/rss:
+ get:
+ tags:
+ - files
+ summary: Generates an RSS feed
+ description: Generates an RSS feed for downloaded files
+ operationId: get-rss
+ parameters:
+ - in: query
+ name: params
+ schema:
+ allOf:
+ - $ref: '#/components/schemas/GetAllFilesRequest'
+ - type: object
+ properties:
+ uuid:
+ type: string
+ description: user uid
+ default: null
+ style: form
+ explode: true
+ responses:
+ '200':
+ description: OK
+ content:
+ text/plain:
+ schema:
+ type: string
+ description: RSS feed
+ security:
+ - Auth query parameter: []
/api/getFile:
post:
tags:
@@ -1755,32 +1786,39 @@ components:
description: Two elements allowed, start index and end index
minItems: 2
maxItems: 2
+ default: null
text_search:
type: string
description: Filter files by title
+ default: null
file_type_filter:
$ref: '#/components/schemas/FileTypeFilter'
favorite_filter:
type: boolean
description: If set to true, only gets favorites
+ default: false
sub_id:
type: string
description: Include if you want to filter by subscription
+ default: null
Sort:
type: object
properties:
by:
type: string
description: Property to sort by
+ default: registered
order:
type: number
description: 1 for ascending, -1 for descending
+ default: -1
FileTypeFilter:
type: string
enum:
- audio_only
- video_only
- both
+ default: both
GetAllFilesResponse:
required:
- files
diff --git a/backend/app.js b/backend/app.js
index f4c66ef..90c72ab 100644
--- a/backend/app.js
+++ b/backend/app.js
@@ -18,6 +18,7 @@ const URL = require('url').URL;
const CONSTS = require('./consts')
const read_last_lines = require('read-last-lines');
const ps = require('ps-node');
+const Feed = require('feed').Feed;
// needed if bin/details somehow gets deleted
if (!fs.existsSync(CONSTS.DETAILS_BIN_PATH)) fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, {"version":"2000.06.06","path":"node_modules\\youtube-dl\\bin\\youtube-dl.exe","exec":"youtube-dl.exe","downloader":"youtube-dl"})
@@ -700,7 +701,7 @@ app.use(function(req, res, next) {
next();
} else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key') && req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) {
next();
- } else if (req.path.includes('/api/stream/') || req.path.includes('/api/thumbnail/')) {
+ } else if (req.path.includes('/api/stream/') || req.path.includes('/api/thumbnail/') || req.path.includes('/api/rss')) {
next();
} else {
logger.verbose(`Rejecting request - invalid API use for endpoint: ${req.path}. API key received: ${req.query.apiKey}`);
@@ -923,7 +924,6 @@ app.post('/api/getFile', optionalJwt, async function (req, res) {
app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
// these are returned
- let files = null;
const sort = req.body.sort;
const range = req.body.range;
const text_search = req.body.text_search;
@@ -932,31 +932,7 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
const sub_id = req.body.sub_id;
const uuid = req.isAuthenticated() ? req.user.uid : null;
- const filter_obj = {user_uid: uuid};
- const regex = true;
- if (text_search) {
- if (regex) {
- filter_obj['title'] = {$regex: `.*${text_search}.*`, $options: 'i'};
- } else {
- filter_obj['$text'] = { $search: utils.createEdgeNGrams(text_search) };
- }
- }
-
- if (favorite_filter) {
- filter_obj['favorite'] = true;
- }
-
- 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;
-
- files = await db_api.getRecords('files', filter_obj, false, sort, range, text_search);
- const file_count = await db_api.getRecords('files', filter_obj, true);
-
- files = JSON.parse(JSON.stringify(files));
+ const {files, file_count} = await db_api.getAllFiles(sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid);
res.send({
files: files,
@@ -2043,6 +2019,56 @@ app.post('/api/deleteAllNotifications', optionalJwt, async (req, res) => {
res.send({success: success});
});
+// rss feed
+
+app.get('/api/rss', async function (req, res) {
+ if (!config_api.getConfigItem('ytdl_enable_rss_feed')) {
+ logger.error('RSS feed is disabled! It must be enabled in the settings before it can be generated.');
+ res.sendStatus(403);
+ return;
+ }
+
+ // these are returned
+ const sort = req.query.sort;
+ const range = req.query.range;
+ const text_search = req.query.text_search;
+ const file_type_filter = req.query.file_type_filter;
+ const favorite_filter = req.query.favorite_filter;
+ const sub_id = req.query.sub_id;
+ const uuid = req.query.uuid;
+
+ const {files} = await db_api.getAllFiles(sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid);
+
+ const feed = new Feed({
+ title: 'Downloads',
+ description: 'YoutubeDL-Material downloads',
+ id: utils.getBaseURL(),
+ link: utils.getBaseURL(),
+ image: 'https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/src/assets/images/logo_128px.png',
+ favicon: 'https://raw.githubusercontent.com/Tzahi12345/YoutubeDL-Material/master/src/favicon.ico',
+ generator: 'YoutubeDL-Material'
+ });
+
+ files.forEach(file => {
+ feed.addItem({
+ title: file.title,
+ link: `${utils.getBaseURL()}/#/player;uid=${file.uid}`,
+ description: file.description,
+ author: [
+ {
+ name: file.uploader,
+ link: file.url
+ }
+ ],
+ contributor: [],
+ date: file.timestamp,
+ // https://stackoverflow.com/a/45415677/8088021
+ image: file.thumbnailURL.replace('&', '&')
+ });
+ });
+ res.send(feed.rss2());
+});
+
// web server
app.use(function(req, res, next) {
diff --git a/backend/config.js b/backend/config.js
index 432b382..2d7dff1 100644
--- a/backend/config.js
+++ b/backend/config.js
@@ -202,6 +202,7 @@ const DEFAULT_CONFIG = {
"enable_notifications": true,
"enable_all_notifications": true,
"allowed_notification_types": [],
+ "enable_rss_feed": false,
},
"API": {
"use_API_key": false,
diff --git a/backend/consts.js b/backend/consts.js
index fcb2e1c..e64f9b6 100644
--- a/backend/consts.js
+++ b/backend/consts.js
@@ -92,6 +92,10 @@ exports.CONFIG_ITEMS = {
'key': 'ytdl_allowed_notification_types',
'path': 'YoutubeDLMaterial.Extra.allowed_notification_types'
},
+ 'ytdl_enable_rss_feed': {
+ 'key': 'ytdl_enable_rss_feed',
+ 'path': 'YoutubeDLMaterial.Extra.enable_rss_feed'
+ },
// API
'ytdl_use_api_key': {
diff --git a/backend/db.js b/backend/db.js
index f3c1948..8249ece 100644
--- a/backend/db.js
+++ b/backend/db.js
@@ -538,8 +538,32 @@ exports.getVideo = async (file_uid) => {
return await exports.getRecord('files', {uid: file_uid});
}
-exports.getFiles = async (uuid = null) => {
- return await exports.getRecords('files', {user_uid: uuid});
+exports.getAllFiles = async (sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid) => {
+ const filter_obj = {user_uid: uuid};
+ const regex = true;
+ if (text_search) {
+ if (regex) {
+ filter_obj['title'] = {$regex: `.*${text_search}.*`, $options: 'i'};
+ } else {
+ filter_obj['$text'] = { $search: utils.createEdgeNGrams(text_search) };
+ }
+ }
+
+ if (favorite_filter) {
+ filter_obj['favorite'] = true;
+ }
+
+ 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;
+
+ const files = JSON.parse(JSON.stringify(await exports.getRecords('files', filter_obj, false, sort, range, text_search)));
+ const file_count = await exports.getRecords('files', filter_obj, true);
+
+ return {files, file_count};
}
exports.setVideoProperty = async (file_uid, assignment_obj) => {
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 03bea2c..46d5761 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -1024,6 +1024,14 @@
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
},
+ "feed": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz",
+ "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==",
+ "requires": {
+ "xml-js": "^1.6.11"
+ }
+ },
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -2607,6 +2615,11 @@
"sparse-bitfield": "^3.0.3"
}
},
+ "sax": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
+ },
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@@ -3112,6 +3125,14 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
+ "xml-js": {
+ "version": "1.6.11",
+ "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
+ "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
+ "requires": {
+ "sax": "^1.2.4"
+ }
+ },
"xmlbuilder2": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.0.2.tgz",
diff --git a/backend/package.json b/backend/package.json
index 2ba0ef3..a1a3527 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -27,6 +27,7 @@
"compression": "^1.7.4",
"config": "^3.2.3",
"express": "^4.17.3",
+ "feed": "^4.2.2",
"fluent-ffmpeg": "^2.1.2",
"fs-extra": "^9.0.0",
"gotify": "^1.1.0",
diff --git a/src/app/settings/settings.component.html b/src/app/settings/settings.component.html
index c34301b..a704bc7 100644
--- a/src/app/settings/settings.component.html
+++ b/src/app/settings/settings.component.html
@@ -297,6 +297,17 @@