From 2a16df15897b0acd2b4b150d13f6eb837066f490 Mon Sep 17 00:00:00 2001 From: dammitjeff <44111923+dammitjeff@users.noreply.github.com> Date: Sun, 11 Jan 2026 13:17:01 -0800 Subject: [PATCH 1/9] Add caching headers for static game assets and video thumbnails --- app/server/fireshare/api.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/server/fireshare/api.py b/app/server/fireshare/api.py index 1ff75831..95e47c3a 100644 --- a/app/server/fireshare/api.py +++ b/app/server/fireshare/api.py @@ -19,6 +19,12 @@ from .constants import SUPPORTED_FILE_TYPES from datetime import datetime +def add_cache_headers(response, cache_key, max_age=604800): + """Add cache headers for static assets (default: 7 days).""" + response.headers['Cache-Control'] = f'public, max-age={max_age}, must-revalidate' + response.headers['ETag'] = f'"{cache_key}"' + return response + templates_path = os.environ.get('TEMPLATE_PATH') or 'templates' api = Blueprint('api', __name__, template_folder=templates_path) @@ -291,10 +297,13 @@ def get_video_poster(): video_id = request.args['id'] webm_poster_path = Path(current_app.config["PROCESSED_DIRECTORY"], "derived", video_id, "boomerang-preview.webm") jpg_poster_path = Path(current_app.config["PROCESSED_DIRECTORY"], "derived", video_id, "poster.jpg") + if request.args.get('animated'): - return send_file(webm_poster_path, mimetype='video/webm') + response = send_file(webm_poster_path, mimetype='video/webm') else: - return send_file(jpg_poster_path, mimetype='image/jpg') + response = send_file(jpg_poster_path, mimetype='image/jpg') + + return add_cache_headers(response, video_id) @api.route('/api/video/view', methods=['POST']) def add_video_view(): @@ -811,7 +820,8 @@ def get_game_asset(steamgriddb_id, filename): } mime_type = mime_types.get(ext, 'image/png') - return send_file(asset_path, mimetype=mime_type) + response = send_file(asset_path, mimetype=mime_type) + return add_cache_headers(response, f"{steamgriddb_id}-{filename}") @api.route('/api/games//videos', methods=["GET"]) def get_game_videos(steamgriddb_id): From c4c1b40afb5cfee1174c614e44fecf316d33c2c3 Mon Sep 17 00:00:00 2001 From: dammitjeff <44111923+dammitjeff@users.noreply.github.com> Date: Sun, 11 Jan 2026 13:32:44 -0800 Subject: [PATCH 2/9] removed redundant text in the "link to game" text box in edit mode --- app/client/src/views/Dashboard.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/app/client/src/views/Dashboard.js b/app/client/src/views/Dashboard.js index 6bb15c5b..053395ae 100644 --- a/app/client/src/views/Dashboard.js +++ b/app/client/src/views/Dashboard.js @@ -396,13 +396,10 @@ const Dashboard = ({ authenticated, searchText, cardSize, listStyle }) => { {/* Link to Game Dialog */} - Link Videos to Game - + Link {selectedVideos.size} Clip{selectedVideos.size !== 1 ? 's' : ''} to Game + {!showAddNewGame ? ( <> - - Select a game to link {selectedVideos.size} video{selectedVideos.size > 1 ? 's' : ''} to: - option.name || ''} @@ -415,7 +412,7 @@ const Dashboard = ({ authenticated, searchText, cardSize, listStyle }) => { setSelectedGame(newValue) } }} - renderInput={(params) => } + renderInput={(params) => } renderOption={(props, option) => ( { ) : ( <> - - Search for a game to add and link {selectedVideos.size} video{selectedVideos.size > 1 ? 's' : ''} to: - From 7fe03ae2913f338f07e4a7cefd2339ee6f033cd7 Mon Sep 17 00:00:00 2001 From: dammitjeff <44111923+dammitjeff@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:53:21 -0800 Subject: [PATCH 3/9] added basic fuzzy autodetection for games --- .../src/components/admin/CompactVideoCard.js | 40 +++- .../src/components/game/GameDetectionCard.js | 181 ++++++++++++++++++ app/client/src/services/VideoService.js | 6 + app/server/fireshare/api.py | 29 +++ app/server/fireshare/cli.py | 60 ++++++ app/server/fireshare/util.py | 88 ++++++++- app/server/requirements.txt | 3 +- 7 files changed, 401 insertions(+), 6 deletions(-) create mode 100644 app/client/src/components/game/GameDetectionCard.js diff --git a/app/client/src/components/admin/CompactVideoCard.js b/app/client/src/components/admin/CompactVideoCard.js index 5cf4c0b3..9a5fa07d 100644 --- a/app/client/src/components/admin/CompactVideoCard.js +++ b/app/client/src/components/admin/CompactVideoCard.js @@ -10,6 +10,7 @@ import VideoService from '../../services/VideoService' import _ from 'lodash' import UpdateDetailsModal from '../modal/UpdateDetailsModal' import LightTooltip from '../misc/LightTooltip' +import GameDetectionCard from '../game/GameDetectionCard' const URL = getUrl() const PURL = getPublicWatchUrl() @@ -36,6 +37,8 @@ const CompactVideoCard = ({ const [privateView, setPrivateView] = React.useState(video.info?.private) const [detailsModalOpen, setDetailsModalOpen] = React.useState(false) + const [gameSuggestion, setGameSuggestion] = React.useState(null) + const [showSuggestion, setShowSuggestion] = React.useState(true) const previousVideoRef = React.useRef() const previousVideo = previousVideoRef.current @@ -51,6 +54,25 @@ const CompactVideoCard = ({ previousVideoRef.current = video }) + React.useEffect(() => { + // Fetch game suggestion when component mounts + VideoService.getGameSuggestion(video.video_id) + .then((response) => { + if (response.data) { + setGameSuggestion(response.data) + setShowSuggestion(true) + } + }) + .catch(() => { + // No suggestion or error - that's fine + }) + }, [video.video_id]) + + const handleSuggestionComplete = () => { + setShowSuggestion(false) + setGameSuggestion(null) + } + const debouncedMouseEnter = React.useRef( _.debounce(() => { setHover(true) @@ -304,8 +326,8 @@ const CompactVideoCard = ({ width: cardWidth, minHeight: previewVideoHeight, border: '1px solid #3399FFAE', - borderBottomRightRadius: '6px', - borderBottomLeftRadius: '6px', + borderBottomRightRadius: (authenticated && gameSuggestion && showSuggestion && !editMode) ? 0 : '6px', + borderBottomLeftRadius: (authenticated && gameSuggestion && showSuggestion && !editMode) ? 0 : '6px', borderTop: 'none', background: 'repeating-linear-gradient(45deg,#606dbc,#606dbc 10px,#465298 10px,#465298 20px)', overflow: 'hidden' @@ -326,8 +348,8 @@ const CompactVideoCard = ({ WebkitAnimationDuration: '1.5s', WebkitAnimationFillMode: 'both', border: '1px solid #3399FFAE', - borderBottomRightRadius: '6px', - borderBottomLeftRadius: '6px', + borderBottomRightRadius: (authenticated && gameSuggestion && showSuggestion && !editMode) ? 0 : '6px', + borderBottomLeftRadius: (authenticated && gameSuggestion && showSuggestion && !editMode) ? 0 : '6px', borderTop: 'none', overflow: 'hidden' }} @@ -398,6 +420,16 @@ const CompactVideoCard = ({ + + {/* Game Detection Suggestion Card */} + {authenticated && gameSuggestion && showSuggestion && !editMode && ( + + )} ) diff --git a/app/client/src/components/game/GameDetectionCard.js b/app/client/src/components/game/GameDetectionCard.js new file mode 100644 index 00000000..2c8bc0b5 --- /dev/null +++ b/app/client/src/components/game/GameDetectionCard.js @@ -0,0 +1,181 @@ +import React, { useState } from 'react' +import { Box, IconButton, Typography, Fade } from '@mui/material' +import CheckIcon from '@mui/icons-material/Check' +import CloseIcon from '@mui/icons-material/Close' +import SportsEsportsIcon from '@mui/icons-material/SportsEsports' +import VideoService from '../../services/VideoService' +import GameService from '../../services/GameService' + +/** + * GameDetectionCard - Shows automatic game detection suggestions + * Appears below video thumbnails when a game is detected from the filename + */ +export default function GameDetectionCard({ videoId, suggestion, onComplete, cardWidth }) { + const [loading, setLoading] = useState(false) + const [status, setStatus] = useState('pending') // 'pending', 'accepted', 'rejected' + + const handleAccept = async (e) => { + e.stopPropagation() // Prevent triggering video card click + setLoading(true) + + try { + let gameId = suggestion.game_id + + // If game doesn't exist in our DB (came from SteamGridDB), create it first + if (!gameId && suggestion.steamgriddb_id) { + // Reuse the same logic as GameSearch.js + const assets = (await GameService.getGameAssets(suggestion.steamgriddb_id)).data + const gameData = { + steamgriddb_id: suggestion.steamgriddb_id, + name: suggestion.game_name, + hero_url: assets.hero_url, + logo_url: assets.logo_url, + icon_url: assets.icon_url, + } + const createdGame = (await GameService.createGame(gameData)).data + gameId = createdGame.id + } + + // Link video to game using existing service + await GameService.linkVideoToGame(videoId, gameId) + + // Remove the suggestion from cache + await VideoService.rejectGameSuggestion(videoId) + + setStatus('accepted') + // Auto-hide after showing success + setTimeout(() => { + onComplete?.() + }, 2000) + } catch (err) { + console.error('Failed to accept game suggestion:', err) + setLoading(false) + } + } + + const handleReject = async (e) => { + e.stopPropagation() // Prevent triggering video card click + setLoading(true) + + try { + await VideoService.rejectGameSuggestion(videoId) + setStatus('rejected') + // Hide immediately + setTimeout(() => { + onComplete?.() + }, 300) + } catch (err) { + console.error('Failed to reject game suggestion:', err) + setLoading(false) + } + } + + if (status === 'accepted') { + return ( + + + + + Linked to {suggestion.game_name} + + + + ) + } + + if (status === 'rejected') { + return null + } + + return ( + + e.stopPropagation()} // Prevent triggering video card click + sx={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + p: 1, + width: cardWidth, + background: 'rgba(50, 50, 50, 0.95)', + borderLeft: '1px solid #3399FFAE', + borderRight: '1px solid #3399FFAE', + borderBottom: '1px solid #3399FFAE', + borderBottomLeftRadius: '6px', + borderBottomRightRadius: '6px', + lineHeight: 0, + }} + > + + + + Detected: {suggestion.game_name} + + + + + + + + + + + + + ) +} diff --git a/app/client/src/services/VideoService.js b/app/client/src/services/VideoService.js index d11b89ad..d3458b0c 100644 --- a/app/client/src/services/VideoService.js +++ b/app/client/src/services/VideoService.js @@ -116,6 +116,12 @@ const service = { scan() { return Api().get('/api/manual/scan') }, + getGameSuggestion(videoId) { + return Api().get(`/api/videos/${videoId}/game/suggestion`) + }, + rejectGameSuggestion(videoId) { + return Api().delete(`/api/videos/${videoId}/game/suggestion`) + }, } export default service diff --git a/app/server/fireshare/api.py b/app/server/fireshare/api.py index 95e47c3a..9d5ee2bf 100644 --- a/app/server/fireshare/api.py +++ b/app/server/fireshare/api.py @@ -914,6 +914,35 @@ def delete_game(steamgriddb_id): logger.info(f"Successfully deleted game {game.name}") return Response(status=200) +@api.route('/api/videos//game/suggestion', methods=["GET"]) +def get_video_game_suggestion(video_id): + """Get automatic game detection suggestion for a video""" + from fireshare.cli import get_game_suggestion + + suggestion = get_game_suggestion(video_id) + if not suggestion: + return jsonify(None) + + return jsonify({ + 'video_id': video_id, + 'game_id': suggestion.get('game_id'), + 'game_name': suggestion.get('game_name'), + 'steamgriddb_id': suggestion.get('steamgriddb_id'), + 'confidence': suggestion.get('confidence'), + 'source': suggestion.get('source') + }) + +@api.route('/api/videos//game/suggestion', methods=["DELETE"]) +@login_required +def reject_game_suggestion(video_id): + """User rejected the game suggestion - remove from storage""" + from fireshare.cli import delete_game_suggestion + + if delete_game_suggestion(video_id): + logger.info(f"User rejected game suggestion for video {video_id}") + + return Response(status=204) + @api.after_request def after_request(response): response.headers.add('Accept-Ranges', 'bytes') diff --git a/app/server/fireshare/cli.py b/app/server/fireshare/cli.py index ac46efbb..36c472f8 100755 --- a/app/server/fireshare/cli.py +++ b/app/server/fireshare/cli.py @@ -15,6 +15,54 @@ from .constants import SUPPORTED_FILE_EXTENSIONS +# Helper functions for persistent game suggestions storage +def _get_suggestions_file(): + """Get path to the suggestions JSON file""" + from flask import current_app + data_dir = Path(current_app.config.get('DATA_DIRECTORY', '/data')) + return data_dir / 'game_suggestions.json' + +def _load_suggestions(): + """Load suggestions from JSON file""" + suggestions_file = _get_suggestions_file() + if suggestions_file.exists(): + try: + with open(suggestions_file, 'r') as f: + return json.load(f) + except (json.JSONDecodeError, IOError): + return {} + return {} + +def _save_suggestions(suggestions): + """Save suggestions to JSON file""" + suggestions_file = _get_suggestions_file() + suggestions_file.parent.mkdir(parents=True, exist_ok=True) + try: + with open(suggestions_file, 'w') as f: + json.dump(suggestions, f) + except IOError as e: + logger.error(f"Failed to save game suggestions: {e}") + +def get_game_suggestion(video_id): + """Get a game suggestion for a video""" + suggestions = _load_suggestions() + return suggestions.get(video_id) + +def save_game_suggestion(video_id, suggestion): + """Save a game suggestion for a video""" + suggestions = _load_suggestions() + suggestions[video_id] = suggestion + _save_suggestions(suggestions) + +def delete_game_suggestion(video_id): + """Delete a game suggestion for a video""" + suggestions = _load_suggestions() + if video_id in suggestions: + del suggestions[video_id] + _save_suggestions(suggestions) + return True + return False + def send_discord_webhook(webhook_url=None, video_url=None): payload = { "content": video_url, @@ -249,6 +297,18 @@ def scan_video(ctx, path): db.session.add(info) db.session.commit() + # Automatic game detection + logger.info("Attempting automatic game detection...") + steamgriddb_api_key = config.get("integrations", {}).get("steamgriddb_api_key") + filename = Path(v.path).stem + detected_game = util.detect_game_from_filename(filename, steamgriddb_api_key) + + if detected_game and detected_game['confidence'] >= 0.65: + save_game_suggestion(v.video_id, detected_game) + logger.info(f"Created game suggestion for video {v.video_id}: {detected_game['game_name']} (confidence: {detected_game['confidence']:.2f}, source: {detected_game['source']})") + else: + logger.info(f"No confident game match found for video {v.video_id}") + logger.info("Syncing metadata") ctx.invoke(sync_metadata, video=video_id) info = VideoInfo.query.filter(VideoInfo.video_id==video_id).one() diff --git a/app/server/fireshare/util.py b/app/server/fireshare/util.py index d956cb49..55a12ca4 100644 --- a/app/server/fireshare/util.py +++ b/app/server/fireshare/util.py @@ -582,4 +582,90 @@ def seconds_to_dur_string(sec): if hours: return ':'.join([str(hours), str(mins).zfill(2), str(s).zfill(2)]) else: - return ':'.join([str(mins), str(s).zfill(2)]) \ No newline at end of file + return ':'.join([str(mins), str(s).zfill(2)]) + +def detect_game_from_filename(filename: str, steamgriddb_api_key: str = None): + """ + Fuzzy match a video filename against existing games in database using RapidFuzz. + Falls back to SteamGridDB search if no local match found. + + Args: + filename: Video filename without extension + steamgriddb_api_key: Optional API key for SteamGridDB fallback + + Returns: + dict with 'game_id', 'game_name', 'steamgriddb_id', 'confidence', 'source' or None + """ + from rapidfuzz import fuzz, process + from fireshare.models import GameMetadata + import re + + # Clean filename for better matching + clean_name = filename.lower() + # Remove common patterns: dates, numbers, "gameplay", etc. + clean_name = re.sub(r'\d{4}-\d{2}-\d{2}', '', clean_name) # Remove dates like 2024-01-14 + clean_name = re.sub(r'\d{8}', '', clean_name) # Remove YYYYMMDD format + clean_name = re.sub(r'\b(gameplay|clip|highlights?|match|game|recording|video)\b', '', clean_name, flags=re.IGNORECASE) + clean_name = re.sub(r'[_\-]+', ' ', clean_name) # Replace _ and - with spaces + clean_name = re.sub(r'\s+', ' ', clean_name) # Normalize whitespace + clean_name = clean_name.strip() + + if not clean_name: + logger.debug("Filename cleaned to empty string, cannot detect game") + return None + + # Step 1: Try local database first + games = GameMetadata.query.all() + + if not games: + logger.debug("No games in database to match against") + else: + # Create list of (game_name, game_object) tuples for rapidfuzz + game_choices = [(game.name, game) for game in games] + + # Use token_set_ratio - ignores word order and extra words + result = process.extractOne( + clean_name, + game_choices, + scorer=fuzz.token_set_ratio, + score_cutoff=65 # Minimum confidence (0-100 scale) + ) + + if result: + matched_name, score, matched_game = result[0], result[1], result[2] + best_match = { + 'game_id': matched_game.id, + 'game_name': matched_game.name, + 'steamgriddb_id': matched_game.steamgriddb_id, + 'confidence': score / 100, # Convert to 0-1 scale + 'source': 'local' + } + logger.info(f"Local game match: {best_match['game_name']} (confidence: {score:.0f}%)") + return best_match + + # Step 2: Fallback to SteamGridDB search + if steamgriddb_api_key: + logger.info(f"No local match found, searching SteamGridDB for: '{clean_name}'") + from fireshare.steamgrid import SteamGridDBClient + client = SteamGridDBClient(steamgriddb_api_key) + + try: + results = client.search_games(clean_name) + if results and len(results) > 0: + # Take the first result (SteamGridDB returns best matches first) + top_result = results[0] + detected = { + 'game_id': None, # Not in our DB yet + 'game_name': top_result.get('name'), + 'steamgriddb_id': top_result.get('id'), + 'confidence': 0.75, # Assume SteamGridDB results are good + 'source': 'steamgriddb', + 'release_date': top_result.get('release_date') + } + logger.info(f"SteamGridDB match: {detected['game_name']} (id: {detected['steamgriddb_id']})") + return detected + except Exception as ex: + logger.warning(f"SteamGridDB search failed: {ex}") + + logger.debug(f"No game match found for filename: '{clean_name}'") + return None \ No newline at end of file diff --git a/app/server/requirements.txt b/app/server/requirements.txt index eb334a0e..5f40ff58 100644 --- a/app/server/requirements.txt +++ b/app/server/requirements.txt @@ -21,4 +21,5 @@ zipp==3.8.0 xxhash==3.0.0 apscheduler==3.9.1 python-ldap==3.4.3 -requests==2.27.1 \ No newline at end of file +requests==2.27.1 +rapidfuzz==3.6.1 \ No newline at end of file From f4c8dbfd767377339ab632cb5b08ff187b117926 Mon Sep 17 00:00:00 2001 From: dammitjeff <44111923+dammitjeff@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:06:25 -0800 Subject: [PATCH 4/9] added manual scan for games in settings --- app/client/src/services/VideoService.js | 3 ++ app/client/src/views/Settings.js | 23 ++++++++++++- app/server/fireshare/api.py | 46 +++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/app/client/src/services/VideoService.js b/app/client/src/services/VideoService.js index d3458b0c..e639e7e6 100644 --- a/app/client/src/services/VideoService.js +++ b/app/client/src/services/VideoService.js @@ -116,6 +116,9 @@ const service = { scan() { return Api().get('/api/manual/scan') }, + scanGames() { + return Api().get('/api/manual/scan-games') + }, getGameSuggestion(videoId) { return Api().get(`/api/videos/${videoId}/game/suggestion`) }, diff --git a/app/client/src/views/Settings.js b/app/client/src/views/Settings.js index 5f3cfe48..4f8466b7 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 SportsEsportsIcon from '@mui/icons-material/SportsEsports' import VisibilityIcon from '@mui/icons-material/Visibility' import VisibilityOffIcon from '@mui/icons-material/VisibilityOff' import { ConfigService, VideoService } from '../services' @@ -88,6 +89,23 @@ const Settings = ({ authenticated }) => { }) } + const handleScanGames = async () => { + try { + const response = await VideoService.scanGames() + setAlert({ + open: true, + type: 'success', + message: `Game scan complete! Created ${response.data.suggestions_created} suggestions from ${response.data.total_videos} videos.`, + }) + } catch (err) { + setAlert({ + open: true, + type: 'error', + message: err.response?.data?.error || 'Failed to scan videos for games', + }) + } + } + const checkForWarnings = async () =>{ let warnings = await WarningService.getAdminWarnings() @@ -375,10 +393,13 @@ const Settings = ({ authenticated }) => { - + + diff --git a/app/server/fireshare/api.py b/app/server/fireshare/api.py index 9d5ee2bf..ac72fa0b 100644 --- a/app/server/fireshare/api.py +++ b/app/server/fireshare/api.py @@ -142,6 +142,52 @@ def manual_scan(): Popen(["fireshare", "bulk-import"], shell=False) return Response(status=200) +@api.route('/api/manual/scan-games') +@login_required +def manual_scan_games(): + """Scan all videos for game detection and create suggestions""" + from fireshare import util + from fireshare.cli import save_game_suggestion, _load_suggestions + + try: + steamgriddb_api_key = get_steamgriddb_api_key() + + # Get all videos + videos = Video.query.join(VideoInfo).all() + suggestions_created = 0 + + # Load existing suggestions to avoid duplicates + existing_suggestions = _load_suggestions() + + for video in videos: + # Skip if already has a game linked + existing_link = VideoGameLink.query.filter_by(video_id=video.video_id).first() + if existing_link: + continue + + # Skip if already has a suggestion + if video.video_id in existing_suggestions: + continue + + # Try to detect game from filename + filename = Path(video.path).stem + detected_game = util.detect_game_from_filename(filename, steamgriddb_api_key) + + if detected_game and detected_game['confidence'] >= 0.65: + save_game_suggestion(video.video_id, detected_game) + suggestions_created += 1 + logger.info(f"Created game suggestion for video {video.video_id}: {detected_game['game_name']}") + + return jsonify({ + 'success': True, + 'total_videos': len(videos), + 'suggestions_created': suggestions_created + }), 200 + + except Exception as e: + logger.error(f"Error scanning videos for games: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + @api.route('/api/videos') @login_required def get_videos(): From b7a6da50e256c5eae6a10cf1125b9f17f1b605be Mon Sep 17 00:00:00 2001 From: dammitjeff <44111923+dammitjeff@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:51:21 -0800 Subject: [PATCH 5/9] checks if existing game is in database, preventing duplicates --- app/server/fireshare/api.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/server/fireshare/api.py b/app/server/fireshare/api.py index ac72fa0b..963c99be 100644 --- a/app/server/fireshare/api.py +++ b/app/server/fireshare/api.py @@ -715,6 +715,20 @@ def create_game(): if not data.get('steamgriddb_id'): return Response(status=400, response='SteamGridDB ID is required.') + existing_game = GameMetadata.query.filter_by(steamgriddb_id=data['steamgriddb_id']).first() + if existing_game: + updated = False + if data.get('name') and data['name'] != existing_game.name: + existing_game.name = data['name'] + updated = True + if data.get('release_date') and data.get('release_date') != existing_game.release_date: + existing_game.release_date = data['release_date'] + updated = True + if updated: + existing_game.updated_at = datetime.utcnow() + db.session.commit() + return jsonify(existing_game.json()), 200 + # Get API key and initialize client api_key = get_steamgriddb_api_key() if not api_key: From d825ea4b2e96b6172181f0392f7483b34365e52f Mon Sep 17 00:00:00 2001 From: dammitjeff <44111923+dammitjeff@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:59:32 -0800 Subject: [PATCH 6/9] changing some text --- app/client/src/views/Settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/src/views/Settings.js b/app/client/src/views/Settings.js index 4f8466b7..2bcd4b95 100644 --- a/app/client/src/views/Settings.js +++ b/app/client/src/views/Settings.js @@ -398,7 +398,7 @@ const Settings = ({ authenticated }) => { Scan Library From 7322b926b3679f37a2099835b6ec5c5759ffd37b Mon Sep 17 00:00:00 2001 From: dammitjeff <44111923+dammitjeff@users.noreply.github.com> Date: Wed, 14 Jan 2026 17:49:06 -0800 Subject: [PATCH 7/9] made Games tab visibility optional --- app/client/src/components/nav/Navbar20.js | 10 +++++++- app/client/src/views/Settings.js | 30 +++++++++++++++++++---- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/app/client/src/components/nav/Navbar20.js b/app/client/src/components/nav/Navbar20.js index 10a4264e..0f1d9d47 100644 --- a/app/client/src/components/nav/Navbar20.js +++ b/app/client/src/components/nav/Navbar20.js @@ -49,7 +49,7 @@ const minimizedDrawerWidth = 57 const CARD_SIZE_DEFAULT = 375 const CARD_SIZE_MULTIPLIER = 2 -const pages = [ +const allPages = [ { title: 'My Videos', icon: , href: '/', private: true }, { title: 'Public Videos', icon: , href: '/feed', private: false }, { title: 'Games', icon: , href: '/games', private: false }, @@ -149,6 +149,14 @@ function Navbar20({ const [alert, setAlert] = React.useState({ open: false }) const navigate = useNavigate() + const uiConfig = getSetting('ui_config') || {} + const pages = allPages.filter((p) => { + if (p.href === '/games' && uiConfig.show_games_tab === false) { + return false + } + return true + }) + const handleDrawerToggle = () => { setMobileOpen(!mobileOpen) } diff --git a/app/client/src/views/Settings.js b/app/client/src/views/Settings.js index 2bcd4b95..0a4ae451 100644 --- a/app/client/src/views/Settings.js +++ b/app/client/src/views/Settings.js @@ -299,13 +299,13 @@ const Settings = ({ authenticated }) => { + checked={updatedConfig.ui_config?.autoplay || false} + onChange={(e) => setUpdatedConfig((prev) => ({ ...prev, - ui_config: { - ...prev.ui_config, - autoplay: e.target.checked + ui_config: { + ...prev.ui_config, + autoplay: e.target.checked } })) } @@ -314,6 +314,26 @@ const Settings = ({ authenticated }) => { label="Auto Play Videos" /> + + + Navigation + + + + setUpdatedConfig((prev) => ({ + ...prev, + ui_config: { ...prev.ui_config, show_games_tab: e.target.checked }, + })) + } + /> + } + label="Show Games Tab" + /> + Integrations From b1a4ba52b590c2a861c3bb34073cdece7f1fcf08 Mon Sep 17 00:00:00 2001 From: dammitjeff <44111923+dammitjeff@users.noreply.github.com> Date: Wed, 14 Jan 2026 18:02:09 -0800 Subject: [PATCH 8/9] Added options to disable My Videos and Public Videos --- app/client/src/components/nav/Navbar20.js | 6 ++-- app/client/src/views/Settings.js | 36 ++++++++++++++++++++--- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/app/client/src/components/nav/Navbar20.js b/app/client/src/components/nav/Navbar20.js index 0f1d9d47..ea4fc69d 100644 --- a/app/client/src/components/nav/Navbar20.js +++ b/app/client/src/components/nav/Navbar20.js @@ -151,9 +151,9 @@ function Navbar20({ const uiConfig = getSetting('ui_config') || {} const pages = allPages.filter((p) => { - if (p.href === '/games' && uiConfig.show_games_tab === false) { - return false - } + if (p.href === '/' && uiConfig.show_my_videos === false) return false + if (p.href === '/feed' && uiConfig.show_public_videos === false) return false + if (p.href === '/games' && uiConfig.show_games === false) return false return true }) diff --git a/app/client/src/views/Settings.js b/app/client/src/views/Settings.js index 0a4ae451..11f93503 100644 --- a/app/client/src/views/Settings.js +++ b/app/client/src/views/Settings.js @@ -316,22 +316,50 @@ const Settings = ({ authenticated }) => { - Navigation + Sidebar setUpdatedConfig((prev) => ({ ...prev, - ui_config: { ...prev.ui_config, show_games_tab: e.target.checked }, + ui_config: { ...prev.ui_config, show_my_videos: e.target.checked }, })) } /> } - label="Show Games Tab" + label="My Videos" + /> + + setUpdatedConfig((prev) => ({ + ...prev, + ui_config: { ...prev.ui_config, show_public_videos: e.target.checked }, + })) + } + /> + } + label="Public Videos" + /> + + setUpdatedConfig((prev) => ({ + ...prev, + ui_config: { ...prev.ui_config, show_games: e.target.checked }, + })) + } + /> + } + label="Games" /> From 9729699e03fa29f1ccb6562b358a468773773310 Mon Sep 17 00:00:00 2001 From: dammitjeff <44111923+dammitjeff@users.noreply.github.com> Date: Wed, 14 Jan 2026 20:38:40 -0800 Subject: [PATCH 9/9] fixed deleting game bug --- app/server/fireshare/api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/server/fireshare/api.py b/app/server/fireshare/api.py index 963c99be..2bcb4468 100644 --- a/app/server/fireshare/api.py +++ b/app/server/fireshare/api.py @@ -936,6 +936,8 @@ def delete_game(steamgriddb_id): derived_path = paths['processed'] / 'derived' / video.video_id # Delete from database + VideoGameLink.query.filter_by(video_id=video.video_id).delete() + VideoView.query.filter_by(video_id=video.video_id).delete() VideoInfo.query.filter_by(video_id=video.video_id).delete() Video.query.filter_by(video_id=video.video_id).delete()