diff --git a/README.md b/README.md
index a86b7961..50626c43 100644
--- a/README.md
+++ b/README.md
@@ -125,6 +125,12 @@ Direct links copied from the link copy buttons in Fireshare will allow websites
+
RSS Feed Support
+
+ Stay up to date in your favorite RSS reader. Fireshare publishes a feed of the
+ latest public videos so you can subscribe and get new uploads automatically.
+
+
LDAP Authentication Support
diff --git a/app/client/src/components/nav/Navbar20.js b/app/client/src/components/nav/Navbar20.js
index a0481e4a..33f9a602 100644
--- a/app/client/src/components/nav/Navbar20.js
+++ b/app/client/src/components/nav/Navbar20.js
@@ -429,8 +429,8 @@ function Navbar20({
-
-
+
+
{folderSize !== null ? (
open ? (
-
+
)
) : (
-
+
{open ? Loading Disk Usage... : }
+ />}
)}
diff --git a/app/client/src/views/Settings.js b/app/client/src/views/Settings.js
index ab87f598..4853c957 100644
--- a/app/client/src/views/Settings.js
+++ b/app/client/src/views/Settings.js
@@ -14,6 +14,7 @@ import {
import SnackbarAlert from '../components/alert/SnackbarAlert'
import SaveIcon from '@mui/icons-material/Save'
import SensorsIcon from '@mui/icons-material/Sensors'
+import RssFeedIcon from '@mui/icons-material/RssFeed'
import SportsEsportsIcon from '@mui/icons-material/SportsEsports'
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'
import VisibilityIcon from '@mui/icons-material/Visibility'
@@ -41,9 +42,18 @@ const Settings = ({ authenticated }) => {
React.useEffect(() => {
async function fetch() {
try {
- const conf = (await ConfigService.getAdminConfig()).data
- setConfig(conf)
- setUpdatedConfig(conf)
+ const res = await ConfigService.getAdminConfig()
+ const conf = _.cloneDeep(res.data)
+
+ // Ensure rss_config exists and has default values for comparison
+ if (!conf.rss_config) {
+ conf.rss_config = { title: '', description: '' }
+ }
+ if (!conf.rss_config.title) conf.rss_config.title = ''
+ if (!conf.rss_config.description) conf.rss_config.description = ''
+
+ setConfig(_.cloneDeep(conf))
+ setUpdatedConfig(_.cloneDeep(conf))
await checkForWarnings()
} catch (err) {
console.error(err)
@@ -53,7 +63,9 @@ const Settings = ({ authenticated }) => {
}, [])
React.useEffect(() => {
- setUpdateable(!_.isEqual(config, updatedConfig))
+ if (config && updatedConfig) {
+ setUpdateable(!_.isEqual(config, updatedConfig))
+ }
}, [updatedConfig, config])
React.useEffect(() => {
@@ -66,11 +78,11 @@ const Settings = ({ authenticated }) => {
try {
await ConfigService.updateConfig(updatedConfig)
setUpdateable(false)
- setConfig((prev) => ({ ...prev, ...updatedConfig }))
+ setConfig(_.cloneDeep(updatedConfig))
setAlert({ open: true, message: 'Settings Updated! Changes may take a minute to take effect.', type: 'success' })
} catch (err) {
console.error(err)
- setAlert({ open: true, message: err.response.data, type: 'error' })
+ setAlert({ open: true, message: err.response?.data || 'Error saving settings', type: 'error' })
}
}
@@ -135,41 +147,41 @@ const Settings = ({ authenticated }) => {
const checkForWarnings = async () =>{
let warnings = await WarningService.getAdminWarnings()
- if (Object.keys(warnings.data).length === 0)
- return;
+ if (Object.keys(warnings.data).length === 0)
+ return;
- for (const warning of warnings.data) {
- // Check if this is the SteamGridDB warning
- if (warning.includes('SteamGridDB API key not configured')) {
- setAlert({
- open: true,
- type: 'warning',
- message: (
-
- {warning.replace('Click here to set it up.', '')}
- {
- e.preventDefault();
- document.getElementById('steamgrid-api-key-field')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
- document.getElementById('steamgrid-api-key-field')?.focus();
- }}
- style={{ color: '#2684FF', textDecoration: 'underline', cursor: 'pointer', marginLeft: '4px' }}
- >
- Click here to set it up.
-
-
- ),
- });
- } else {
- setAlert({
- open: true,
- type: 'warning',
- message: warning,
- });
- }
- await new Promise(r => setTimeout(r, 2000)); //Without this a second Warning would instantly overwrite the first...
+ for (const warning of warnings.data) {
+ // Check if this is the SteamGridDB warning
+ if (warning.includes('SteamGridDB API key not configured')) {
+ setAlert({
+ open: true,
+ type: 'warning',
+ message: (
+
+ {warning.replace('Click here to set it up.', '')}
+ {
+ e.preventDefault();
+ document.getElementById('steamgrid-api-key-field')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ document.getElementById('steamgrid-api-key-field')?.focus();
+ }}
+ style={{ color: '#2684FF', textDecoration: 'underline', cursor: 'pointer', marginLeft: '4px' }}
+ >
+ Click here to set it up.
+
+
+ ),
+ });
+ } else {
+ setAlert({
+ open: true,
+ type: 'warning',
+ message: warning,
+ });
}
+ await new Promise(r => setTimeout(r, 2000)); //Without this a second Warning would instantly overwrite the first...
+ }
}
return (
@@ -467,10 +479,50 @@ const Settings = ({ authenticated }) => {
),
}}
/>
+
+
+
+ Feeds
+
+
+
+ setUpdatedConfig((prev) => ({
+ ...prev,
+ rss_config: { ...(prev.rss_config || {}), title: e.target.value },
+ }))
+ }
+ />
+
+ setUpdatedConfig((prev) => ({
+ ...prev,
+ rss_config: { ...(prev.rss_config || {}), description: e.target.value },
+ }))
+ }
+ />
+ }
+ fullWidth
+ onClick={() => window.open('/api/feed/rss', '_blank')}
+ sx={{ borderColor: 'rgba(255, 255, 255, 0.23)', color: '#fff' }}
+ >
+ Open RSS Feed
+
+
}
- disabled={!updateable || (!isValidDiscordWebhook(discordUrl) && isDiscordUsed) }
+ disabled={!updateable || (!isValidDiscordWebhook(discordUrl) && isDiscordUsed)}
onClick={handleSave}
>
Save Changes
diff --git a/app/server/fireshare/api.py b/app/server/fireshare/api.py
index 11aaca92..307e7843 100644
--- a/app/server/fireshare/api.py
+++ b/app/server/fireshare/api.py
@@ -1603,6 +1603,70 @@ def clear_all_corrupt_status():
count = clear_all_corrupt_videos()
return jsonify({'cleared': count})
+@api.route('/api/feed/rss')
+def rss_feed():
+ # Base URL for API calls (backend)
+ backend_domain = f"https://{current_app.config['DOMAIN']}" if current_app.config['DOMAIN'] else request.host_url.rstrip('/')
+
+ # URL for viewing (frontend)
+ # If we are on localhost:5000, the user wants both the link and video to point to the public dev port (3000)
+ frontend_domain = backend_domain
+ if "localhost:5000" in frontend_domain:
+ frontend_domain = frontend_domain.replace("localhost:5000", "localhost:3000")
+ elif "127.0.0.1:5000" in frontend_domain:
+ frontend_domain = frontend_domain.replace("127.0.0.1:5000", "localhost:3000")
+
+ # Load custom RSS config if it exists
+ paths = current_app.config['PATHS']
+ config_path = paths['data'] / 'config.json'
+ rss_title = "Fireshare Feed"
+ rss_description = "Latest videos from Fireshare"
+ if config_path.exists():
+ try:
+ with config_path.open() as f:
+ config = json.load(f)
+ rss_title = config.get("rss_config", {}).get("title", rss_title)
+ rss_description = config.get("rss_config", {}).get("description", rss_description)
+ except:
+ pass
+
+ # Only show public and available videos
+ videos = Video.query.join(VideoInfo).filter(
+ VideoInfo.private.is_(False),
+ Video.available.is_(True)
+ ).order_by(Video.created_at.desc()).limit(50).all()
+
+ rss_items = []
+ for video in videos:
+ # Construct URLs
+ link = f"{frontend_domain}/#/w/{video.video_id}"
+ # Point both player link and video stream to the frontend port (3000) as requested
+ video_url = f"{frontend_domain}/api/video?id={video.video_id}"
+ poster_url = f"{frontend_domain}/api/video/poster?id={video.video_id}"
+
+ # XML escaping for description and title is handled by Jinja2 by default,
+ # but we should ensure dates are in RFC 822 format.
+ item = {
+ 'title': video.info.title if video.info else video.video_id,
+ 'link': link,
+ 'description': video.info.description if video.info and video.info.description else f"Video: {video.info.title if video.info else video.video_id}",
+ 'pubDate': video.created_at.strftime('%a, %d %b %Y %H:%M:%S +0000') if video.created_at else '',
+ 'guid': video.video_id,
+ 'enclosure': {
+ 'url': video_url,
+ 'type': 'video/mp4' # Or appropriate mimetype
+ },
+ 'media_thumbnail': poster_url
+ }
+ rss_items.append(item)
+
+ now_str = datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S +0000')
+
+ return Response(
+ render_template('rss.xml', items=rss_items, domain=frontend_domain, now=now_str, feed_title=rss_title, feed_description=rss_description),
+ mimetype='application/rss+xml'
+ )
+
@api.after_request
def after_request(response):
response.headers.add('Accept-Ranges', 'bytes')
diff --git a/app/server/fireshare/constants.py b/app/server/fireshare/constants.py
index 89e26adb..37f0d37e 100644
--- a/app/server/fireshare/constants.py
+++ b/app/server/fireshare/constants.py
@@ -15,6 +15,10 @@
"integrations": {
"discord_webhook_url": "",
"steamgriddb_api_key": "",
+ },
+ "rss_config": {
+ "title": "Fireshare Feed",
+ "description": "Latest videos from Fireshare"
}
}
diff --git a/app/server/fireshare/templates/rss.xml b/app/server/fireshare/templates/rss.xml
new file mode 100644
index 00000000..20eb4192
--- /dev/null
+++ b/app/server/fireshare/templates/rss.xml
@@ -0,0 +1,27 @@
+
+
+
+ {{ feed_title }}
+ {{ domain }}
+ {{ feed_description }}
+ en-us
+ {{ now }}
+ {{ now }}
+ 60
+
+ {% for item in items %}
+
+ {{ item.title }}
+ {{ item.link }}
+ {{ item.description }}
+ {{ item.pubDate }}
+ {{ item.guid }}
+ Fireshare
+
+
+
+
+ {% endfor %}
+
+
+