Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/client/src/services/ConfigService.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ const service = {
config,
})
},
resetDatabase(options) {
const payload = options ? { options } : undefined
return Api().post('/api/admin/reset-database', payload)
},
}

export default service
165 changes: 164 additions & 1 deletion app/client/src/views/Settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import {
Box,
Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Divider,
FormControlLabel,
Grid,
Expand All @@ -15,6 +20,7 @@ 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 DeleteForeverIcon from '@mui/icons-material/DeleteForever'
import VisibilityIcon from '@mui/icons-material/Visibility'
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'
import { ConfigService, VideoService } from '../services'
Expand All @@ -35,7 +41,21 @@ const Settings = ({ authenticated }) => {
const [updateable, setUpdateable] = React.useState(false)
const [discordUrl, setDiscordUrl] = React.useState('')
const [showSteamGridKey, setShowSteamGridKey] = React.useState(false)
const [resetDialogOpen, setResetDialogOpen] = React.useState(false)
const [resetting, setResetting] = React.useState(false)
const defaultResetOptions = {
game_links: false,
game_suggestions: false,
game_metadata: false,
game_assets: false,
video_views: false,
video_metadata: false,
videos: false,
processed_files: false,
}
const [resetOptions, setResetOptions] = React.useState(defaultResetOptions)
const isDiscordUsed = discordUrl.trim() !== ''
const hasResetSelection = Object.values(resetOptions).some(Boolean)


React.useEffect(() => {
Expand Down Expand Up @@ -115,6 +135,33 @@ const Settings = ({ authenticated }) => {
}
}

const handleResetDatabase = async () => {
setResetting(true)
try {
await ConfigService.resetDatabase(resetOptions)
setResetDialogOpen(false)
setAlert({
open: true,
type: 'success',
message: 'Database reset successfully. You can now run a fresh scan.',
})
} catch (err) {
console.error(err)
setAlert({
open: true,
type: 'error',
message: err.response?.data || 'Failed to reset database',
})
} finally {
setResetting(false)
}
}

const openResetDialog = () => {
setResetOptions(defaultResetOptions)
setResetDialogOpen(true)
}

const checkForWarnings = async () =>{
let warnings = await WarningService.getAdminWarnings()

Expand Down Expand Up @@ -457,12 +504,128 @@ const Settings = ({ authenticated }) => {
<Button variant="contained" startIcon={<SportsEsportsIcon />} onClick={handleScanGames}>
Start Manual Scan for Missing Games
</Button>
<Button
variant="contained"
color="error"
startIcon={<DeleteForeverIcon />}
onClick={openResetDialog}
disabled={resetting}
>
Reset Database
</Button>
</Box>
</Grid>
</Grid>
</Box>
<Dialog open={resetDialogOpen} onClose={() => !resetting && setResetDialogOpen(false)}>
<DialogTitle>Reset Database?</DialogTitle>
<DialogContent>
<DialogContentText>
Select what to remove. Deleting videos also removes video info, views, game links, and processed files.
Deleting game metadata also removes game links and game assets. Your original video files on disk are not
removed.
</DialogContentText>
<Stack spacing={1} sx={{ mt: 2 }}>
<FormControlLabel
control={
<Checkbox
checked={resetOptions.game_links}
onChange={() => setResetOptions((prev) => ({ ...prev, game_links: !prev.game_links }))}
disabled={resetting}
/>
}
label="Game links (unlink clips from games)"
/>
<FormControlLabel
control={
<Checkbox
checked={resetOptions.game_suggestions}
onChange={() => setResetOptions((prev) => ({ ...prev, game_suggestions: !prev.game_suggestions }))}
disabled={resetting}
/>
}
label="Game suggestions"
/>
<FormControlLabel
control={
<Checkbox
checked={resetOptions.game_metadata}
onChange={() => setResetOptions((prev) => ({ ...prev, game_metadata: !prev.game_metadata }))}
disabled={resetting}
/>
}
label="Game metadata (game list)"
/>
<FormControlLabel
control={
<Checkbox
checked={resetOptions.game_assets}
onChange={() => setResetOptions((prev) => ({ ...prev, game_assets: !prev.game_assets }))}
disabled={resetting}
/>
}
label="Game assets (downloaded images)"
/>
<Divider />
<FormControlLabel
control={
<Checkbox
checked={resetOptions.video_views}
onChange={() => setResetOptions((prev) => ({ ...prev, video_views: !prev.video_views }))}
disabled={resetting}
/>
}
label="Video views"
/>
<FormControlLabel
control={
<Checkbox
checked={resetOptions.video_metadata}
onChange={() => setResetOptions((prev) => ({ ...prev, video_metadata: !prev.video_metadata }))}
disabled={resetting}
/>
}
label="Video titles and descriptions (reset to filename)"
/>
<FormControlLabel
control={
<Checkbox
checked={resetOptions.videos}
onChange={() => setResetOptions((prev) => ({ ...prev, videos: !prev.videos }))}
disabled={resetting}
/>
}
label="Videos (remove all videos from the library)"
/>
<FormControlLabel
control={
<Checkbox
checked={resetOptions.processed_files}
onChange={() => setResetOptions((prev) => ({ ...prev, processed_files: !prev.processed_files }))}
disabled={resetting}
/>
}
label="Processed files (symlinks and derived)"
/>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={() => setResetDialogOpen(false)} disabled={resetting}>
Cancel
</Button>
<Button
variant="contained"
color="error"
startIcon={<DeleteForeverIcon />}
onClick={handleResetDatabase}
disabled={resetting || !hasResetSelection}
>
{resetting ? 'Resetting...' : 'Reset Database'}
</Button>
</DialogActions>
</Dialog>
</>
)
}

export default Settings
export default Settings
125 changes: 122 additions & 3 deletions app/server/fireshare/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,119 @@ def get_warnings():
else:
return jsonify(warnings)

@api.route('/api/admin/reset-database', methods=["POST"])
@login_required
def reset_database():
"""Reset selected video and game data while preserving config and user settings"""
try:
paths = current_app.config['PATHS']
payload = request.get_json(silent=True) or {}

if isinstance(payload, dict) and isinstance(payload.get("options"), dict):
options = payload["options"]
elif isinstance(payload, dict):
options = payload
else:
options = {}

if not options:
options = {
"game_suggestions": True,
"game_links": True,
"game_metadata": True,
"game_assets": True,
"video_views": True,
"video_metadata": True,
"videos": True,
"processed_files": True,
}

reset_videos = bool(options.get("videos"))
reset_processed = bool(options.get("processed_files")) or reset_videos
reset_game_metadata = bool(options.get("game_metadata"))
reset_game_links = bool(options.get("game_links")) or reset_game_metadata or reset_videos
reset_game_assets = bool(options.get("game_assets")) or reset_game_metadata
reset_video_views = bool(options.get("video_views")) or reset_videos
reset_video_metadata = bool(options.get("video_metadata"))
reset_game_suggestions = bool(options.get("game_suggestions"))

if reset_game_suggestions:
suggestions_file = paths['data'] / 'game_suggestions.json'
if suggestions_file.exists():
suggestions_file.unlink()
current_app.logger.info("Deleted game_suggestions.json")

if reset_game_links:
VideoGameLink.query.delete()
current_app.logger.info("Deleted all video-game links")

if reset_game_metadata:
GameMetadata.query.delete()
current_app.logger.info("Deleted all game metadata")

if reset_video_views:
VideoView.query.delete()
current_app.logger.info("Deleted all video views")

if reset_video_metadata and not reset_videos:
videos_with_info = Video.query.join(VideoInfo).all()
for video in videos_with_info:
title = Path(video.path).stem
db.session.query(VideoInfo).filter_by(video_id=video.video_id).update({
"title": title,
"description": "",
})
current_app.logger.info("Reset video titles and descriptions")

if reset_videos:
VideoInfo.query.delete()
current_app.logger.info("Deleted all video info")
Video.query.delete()
current_app.logger.info("Deleted all videos")

db.session.commit()

if reset_processed:
video_links_dir = paths['processed'] / 'video_links'
derived_dir = paths['processed'] / 'derived'

if video_links_dir.exists():
shutil.rmtree(video_links_dir)
video_links_dir.mkdir()
current_app.logger.info("Cleared video_links directory")

if derived_dir.exists():
shutil.rmtree(derived_dir)
derived_dir.mkdir()
current_app.logger.info("Cleared derived directory")

if reset_game_assets:
game_assets_dir = paths['data'] / 'game_assets'
if game_assets_dir.exists():
shutil.rmtree(game_assets_dir)
game_assets_dir.mkdir()
current_app.logger.info("Cleared game_assets directory")

current_app.logger.info("Database reset complete")
return jsonify({
'message': 'Database reset successfully',
'reset': {
'game_suggestions': reset_game_suggestions,
'game_links': reset_game_links,
'game_metadata': reset_game_metadata,
'game_assets': reset_game_assets,
'video_views': reset_video_views,
'video_metadata': reset_video_metadata,
'videos': reset_videos,
'processed_files': reset_processed,
},
}), 200

except Exception as e:
db.session.rollback()
current_app.logger.error(f"Failed to reset database: {e}")
return Response(response=f'Failed to reset database: {str(e)}', status=500)

@api.route('/api/manual/scan')
@login_required
def manual_scan():
Expand Down Expand Up @@ -213,15 +326,15 @@ def run_scan():
for i, video in enumerate(videos_to_process):
_game_scan_state['current'] = i + 1

# Try to detect game from filename
# Try to detect game from filename or folder path
filename = Path(video.path).stem
detected_game = util.detect_game_from_filename(filename, steamgriddb_api_key)
detected_game = util.detect_game_from_filename(filename, steamgriddb_api_key, path=video.path)

if detected_game and detected_game['confidence'] >= 0.65:
save_game_suggestion(video.video_id, detected_game)
suggestions_created += 1
_game_scan_state['suggestions_created'] = suggestions_created
logger.info(f"Created game suggestion for video {video.video_id}: {detected_game['game_name']}")
logger.info(f"Created game suggestion for video {video.video_id}: {detected_game['game_name']} (confidence: {detected_game['confidence']:.2f}, source: {detected_game['source']})")

logger.info(f"Game scan complete: {suggestions_created} suggestions created from {len(videos_to_process)} videos")

Expand Down Expand Up @@ -798,6 +911,12 @@ def create_game():
response=f"Failed to download game assets: {result['error']}"
)

# Re-check for existing game after asset download (handles race condition)
existing_game = GameMetadata.query.filter_by(steamgriddb_id=data['steamgriddb_id']).first()
if existing_game:
current_app.logger.info(f"Game {data['name']} was created by another request, returning existing")
return jsonify(existing_game.json()), 200

# Create game metadata (without URL fields - they will be constructed dynamically)
game = GameMetadata(
steamgriddb_id=data['steamgriddb_id'],
Expand Down
Loading