From e61e0796904fd0ea68b1b1cffc356f57fce6dc05 Mon Sep 17 00:00:00 2001 From: Shane Israel Date: Wed, 8 Apr 2026 22:53:19 -0600 Subject: [PATCH 1/3] added troubleshooting guide, clean left over chunked upload file bits --- README.md | 21 +-- TROUBLESHOOTING.md | 262 +++++++++++++++++++++++++++++ app/server/fireshare/__init__.py | 10 ++ app/server/fireshare/api/upload.py | 42 ++++- 4 files changed, 309 insertions(+), 26 deletions(-) create mode 100644 TROUBLESHOOTING.md diff --git a/README.md b/README.md index 0e00ebb4..b65716d6 100644 --- a/README.md +++ b/README.md @@ -214,26 +214,7 @@ If you update models, create a migration and review it before opening a pull req ## Troubleshooting -### Playback Problems - -If playback is unstable: - -- Reduce source file size/bitrate -- Verify upload bandwidth on the host -- Prefer browser-friendly formats (MP4/H.264 is safest) -- Consider enabling transcoding for better compatibility -- Test in another browser to rule out codec/browser limitations - -### Upload Fails Behind Nginx - -Increase proxy limits/timeouts, for example: - -```nginx -client_max_body_size 0; -proxy_read_timeout 999999s; -``` - -If you use a different proxy, apply equivalent upload size and timeout settings there. +See [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) for a full guide covering installation issues, playback problems, permission errors, transcoding, LDAP, and more. --- diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 00000000..ec2f144a --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,262 @@ +# Fireshare Troubleshooting Guide + +## Index + +- [Videos Not Appearing](#videos-not-appearing) +- [Playback Problems](#playback-problems) +- [Upload Fails](#upload-fails) +- [Permission Errors](#permission-errors) +- [Cannot Log In](#cannot-log-in) +- [Sessions Expire on Every Restart](#sessions-expire-on-every-restart) +- [Transcoding Issues](#transcoding-issues) +- [Open Graph / Link Previews Not Working](#open-graph--link-previews-not-working) +- [Webhook Notifications Not Sending](#webhook-notifications-not-sending) +- [LDAP Authentication Issues](#ldap-authentication-issues) +- [Stale Scan Lock](#stale-scan-lock) +- [Corrupt Video Detected](#corrupt-video-detected) +- [Database Errors](#database-errors) + +--- + +## Videos Not Appearing + +Videos are discovered by a background scan that runs every `MINUTES_BETWEEN_VIDEO_SCANS` minutes (default: 5). If a video is not showing up: + +1. **Wait for the next scan.** The scan runs on an interval; new files won't appear instantly. + +2. **Check that your video directory is mounted correctly.** The container expects source videos at `/videos`. Confirm the volume is mapped in your `docker-compose.yml` or `docker run` command: + ```yaml + - /path/to/your/clips:/videos + ``` + +3. **Check supported file extensions.** Only `.mp4`, `.mov`, and `.webm` files are scanned. Files with other extensions are ignored. + +4. **Chunk files are skipped.** Files with extensions like `.part0000` (in-progress uploads) are intentionally ignored until complete. + +5. **macOS sidecar files are skipped.** Files prefixed with `._` are skipped automatically. + +6. **Duplicate detection.** If a video with the same content (by hash) already exists in the database, the new file is skipped. Check if the same clip exists under a different path. + +7. **Check container logs** for scan errors: + ```sh + docker logs fireshare + ``` + +--- + +## Playback Problems + +If video playback is slow, buffering, or failing: + +- **Reduce source file size or bitrate.** High-bitrate files require adequate upload bandwidth on the host. +- **Use browser-compatible formats.** MP4 with H.264 video is the most universally supported format for browser playback. Some codecs (AV1, HEVC) may not play in all browsers. +- **Enable transcoding with H.264.** Setting `ENABLE_TRANSCODING=true` can produce more compatible versions, but only if the encoder preference is set to H.264 in Settings → Transcoding. AV1 transcodes may still not play in all browsers. +- **Test in another browser.** Some codecs are only supported in certain browsers. Chrome supports more formats than Safari/Firefox in some cases. +- **Check `.webm` files.** WebM files must use VP8, VP9, or AV1 video codecs to play natively in browsers. + +--- + +## Upload Fails + +### Behind a Reverse Proxy (Nginx, Traefik, etc.) + +Proxies often have default limits that are too small for large video uploads. + +**Nginx:** Add to your proxy configuration: +```nginx +client_max_body_size 0; +proxy_read_timeout 999999s; +``` + +**Traefik:** Set via entrypoint or middleware configuration to increase read timeout and body size limits. Refer to Traefik documentation for your version. + +**Other proxies:** Apply equivalent upload size and timeout settings. + +### Direct Upload (no proxy) + +- The internal Nginx in the container has no file size restriction by default. +- Gunicorn workers time out after 120 seconds — very large uploads over a slow connection may hit this limit. + +--- + +## Permission Errors + +The container runs as user/group `PUID`/`PGID` (default: `1000`/`1000`). All three mounted directories must be readable and writable by this user. + +**Symptoms:** +- Videos scanned but symlinks fail to create +- Database not initializing on first run +- Transcoded files not appearing in `/processed/derived/` +- Errors containing `Permission denied` in container logs + +**Preferred fix:** Set `PUID` and `PGID` to match the user that already owns your directories: +```yaml +PUID=1001 +PGID=1001 +``` + +Run `id your-username` on the host to find the correct UID/GID values. + +**Alternative:** Change ownership of the directories to match the container's default user: +```sh +chown -R 1000:1000 /path/to/data /path/to/processed /path/to/videos +``` + +> **Note:** On NFS mounts, ensure the NFS export grants the correct UID/GID permissions at the server level regardless of which approach you use. + +--- + +## Cannot Log In + +1. **Default credentials** are `admin` / `admin` if `ADMIN_USERNAME` and `ADMIN_PASSWORD` are not set. + +2. **Credentials set via environment variables are applied on every startup.** If you changed `ADMIN_PASSWORD` in your `docker-compose.yml` and restarted, the admin account password was updated to that value. + +3. **`DISABLE_ADMINCREATE=true` with no existing admin user** will result in no admin account existing. Remove this variable, restart to let the admin account be created, then re-enable it if needed. + +4. **LDAP users cannot log in with local passwords.** If LDAP is enabled and the user was imported via LDAP, they must authenticate through LDAP only. + +5. **Verify admin account state** by checking the database directly: + ```sh + docker exec fireshare sqlite3 /data/db.sqlite "SELECT username, admin FROM user WHERE admin=1 AND ldap=0;" + ``` + +--- + +## Sessions Expire on Every Restart + +If users are logged out every time the container restarts, `SECRET_KEY` is not set. + +Without `SECRET_KEY`, a random key is generated on each startup, invalidating all existing session cookies. + +**Fix:** Set a stable, random value in your environment: +```yaml +SECRET_KEY=some-long-random-string-here +``` + +Generate one with: +```sh +python3 -c "import secrets; print(secrets.token_hex(32))" +``` + +--- + +## Transcoding Issues + +### Transcoded videos not appearing + +1. Confirm `ENABLE_TRANSCODING=true` is set. +2. Check that `auto_transcode` is enabled in Settings → Transcoding within the UI (this writes to `/data/config.json`). +3. Transcoding runs as part of the background scan. Wait for the next scan cycle or check logs for progress. +4. Source videos with a height equal to or less than the target resolution are skipped (e.g., a 720p source will not produce a 1080p transcode). + +### GPU transcoding not working + +1. Confirm your GPU supports NVENC. GTX 1050 or newer is required for H.264; RTX 40 series for AV1. +2. On **Unraid**, you must add `--gpus=all` to Extra Parameters and set `NVIDIA_DRIVER_CAPABILITIES=all`. +3. On standard Docker, add `runtime: nvidia` or `--gpus all` to your compose/run command. +4. If GPU encoding fails, Fireshare automatically falls back to CPU encoding. Check logs to see which encoder is being used. + +### Encoder fallback order + +When `TRANSCODE_GPU=true`: +1. AV1 via GPU (av1_nvenc) — RTX 40 series+ +2. H.264 via GPU (h264_nvenc) — GTX 1050+ +3. AV1 via CPU (libsvtav1) +4. H.264 via CPU (libx264) — universal fallback + +### Transcoding a specific video + +A video can be manually queued for transcoding via the video detail/edit page in the UI. + +--- + +## Open Graph / Link Previews Not Working + +Rich previews when sharing links (Discord, Slack, Twitter/X, etc.) require the `DOMAIN` variable to be set. + +```yaml +DOMAIN=v.example.com +``` + +- Do **not** include `http://` or `https://` — just the bare domain. +- Without this, Open Graph meta tags will have incorrect or empty URLs and social media platforms will not generate previews. + +--- + +## Webhook Notifications Not Sending + +### Discord + +- The `DISCORD_WEBHOOK_URL` must match the format: `https://discord.com/api/webhooks/{id}/{token}` +- An incorrectly formatted URL will cause a validation error on startup — check the container logs. + +### Generic Webhook + +- Both `GENERIC_WEBHOOK_URL` **and** `GENERIC_WEBHOOK_PAYLOAD` must be set together. +- If only one is provided, the app will exit with a fatal error on startup. +- The payload must be valid JSON. + +--- + +## LDAP Authentication Issues + +See [LDAP.md](./LDAP.md) for full setup instructions. + +Common issues: + +- **`LDAP_ENABLE`** must be set to `true` along with all connection variables (`LDAP_URL`, `LDAP_BINDDN`, `LDAP_PASSWORD`, `LDAP_BASEDN`, `LDAP_USER_FILTER`). +- **User filter format:** Use `{input}` as a placeholder for the username entered at login. Example: `uid={input}`. +- **Admin group not working:** Admin group membership is determined via the `memberOf` attribute in LDAP. Ensure your LDAP server populates `memberOf` and that `LDAP_ADMIN_GROUP` matches the full DN of the group. +- **LDAP users appearing as non-admin:** If a user was previously created as a local user before LDAP was enabled, they may have incorrect flags. The LDAP login flow sets the `ldap=true` flag on the user record after first LDAP login. + +--- + +## Stale Scan Lock + +During a video scan, a lock file is created at `/data/fireshare.lock` to prevent concurrent scans. If the scan process crashes without cleaning up, the lock file remains and subsequent scans will not run. + +Fireshare automatically detects and removes stale locks from processes that are no longer running. This happens at the start of each scan cycle. + +If scans appear permanently stuck even after restarting the container, you can manually remove the lock: +```sh +docker exec fireshare rm /data/fireshare.lock +``` + +--- + +## Corrupt Video Detected + +When a video fails validation (during metadata extraction or transcoding), it is recorded in `/data/corrupt_videos.json` and skipped in future scans. + +**Symptoms:** +- A video file exists on disk but never appears in the UI +- Container logs show: `"There may be a corrupt video in your video Directory"` + +**Notes:** +- AV1-encoded source files may be flagged as corrupt due to false positives during initial frame decoding. +- A video marked corrupt can still be manually queued for transcoding via the UI, which uses a more lenient validation pass. + +**To clear the corrupt list manually:** +```sh +docker exec fireshare truncate -s 0 /data/corrupt_videos.json +``` +Then wait for the next scan to re-evaluate the files. + +--- + +## Database Errors + +Fireshare uses SQLite with WAL (Write-Ahead Logging) mode for concurrent access. Most database issues are caused by filesystem problems. + +**Common causes:** +- `/data` is on a network filesystem (NFS, SMB/CIFS) that does not support POSIX file locking — SQLite WAL mode requires proper lock support. Use a local disk or a filesystem that supports `flock`. +- Insufficient disk space on the volume holding `/data`. +- The database file was corrupted by a hard shutdown mid-write. + +**Check database integrity:** +```sh +docker exec fireshare sqlite3 /data/db.sqlite "PRAGMA integrity_check;" +``` + +If the integrity check returns anything other than `ok`, restore from a backup or delete `db.sqlite` to let it be recreated (all video metadata will be re-discovered on the next scan, but custom titles, descriptions, and tags will be lost). diff --git a/app/server/fireshare/__init__.py b/app/server/fireshare/__init__.py index 91f23413..6336cb2b 100644 --- a/app/server/fireshare/__init__.py +++ b/app/server/fireshare/__init__.py @@ -192,6 +192,16 @@ def create_app(init_schedule=False): logger.info(f"Creating subpath directory at {str(subpath.absolute())}") subpath.mkdir(parents=True, exist_ok=True) + # Clean up any leftover chunk files from interrupted uploads + import glob as _glob + chunk_files = _glob.glob(str(paths['video'] / '**' / '*.part[0-9][0-9][0-9][0-9]'), recursive=True) + for chunk_file in chunk_files: + try: + os.remove(chunk_file) + logger.info(f"Removed leftover upload chunk: {chunk_file}") + except OSError as e: + logger.warning(f"Failed to remove leftover upload chunk {chunk_file}: {e}") + # Ensure game_assets directory exists game_assets_dir = paths['data'] / 'game_assets' if not game_assets_dir.is_dir(): diff --git a/app/server/fireshare/api/upload.py b/app/server/fireshare/api/upload.py index efa01d3f..1a6389fe 100644 --- a/app/server/fireshare/api/upload.py +++ b/app/server/fireshare/api/upload.py @@ -132,13 +132,14 @@ def public_upload_videoChunked(): upload_folder = config['app_config']['public_upload_folder_name'] required_files = ['blob'] - required_form_fields = ['chunkPart', 'totalChunks', 'checkSum'] + required_form_fields = ['chunkPart', 'totalChunks', 'checkSum', 'fileSize'] if not all(key in request.files for key in required_files) or not all(key in request.form for key in required_form_fields): return Response(status=400) blob = request.files.get('blob') chunkPart = int(request.form.get('chunkPart')) totalChunks = int(request.form.get('totalChunks')) checkSum = re.sub(r'[^a-zA-Z0-9_-]', '', request.form.get('checkSum')) + fileSize = int(request.form.get('fileSize')) if not checkSum: return Response(status=400) if not blob.filename or blob.filename.strip() == '' or blob.filename == 'blob': @@ -158,23 +159,52 @@ def public_upload_videoChunked(): upload_directory = paths['video'] / upload_folder if not os.path.exists(upload_directory): os.makedirs(upload_directory) - tempPath = os.path.join(upload_directory, f"{checkSum}.{filetype}") + + tempPath = os.path.join(upload_directory, f"{checkSum}.part{chunkPart:04d}") # Guard against path traversal: ensure the resolved path stays within upload_directory if not os.path.realpath(tempPath).startswith(os.path.realpath(upload_directory) + os.sep): return Response(status=400) - with open(tempPath, 'ab') as f: + + with open(tempPath, 'wb') as f: f.write(blob.read()) - if chunkPart < totalChunks: + + # Check if we have all chunks + chunk_files = [] + for i in range(1, totalChunks + 1): + chunk_path = os.path.join(upload_directory, f"{checkSum}.part{i:04d}") + if os.path.exists(chunk_path): + chunk_files.append(chunk_path) + + if len(chunk_files) != totalChunks: return Response(status=202) save_path = os.path.join(upload_directory, filename) - if (os.path.exists(save_path)): + if os.path.exists(save_path): name_no_type = ".".join(filename.split('.')[0:-1]) uid = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(6)) save_path = os.path.join(paths['video'], upload_folder, f"{name_no_type}-{uid}.{filetype}") - os.rename(tempPath, save_path) + try: + with open(save_path, 'wb') as output_file: + for i in range(1, totalChunks + 1): + chunk_path = os.path.join(upload_directory, f"{checkSum}.part{i:04d}") + with open(chunk_path, 'rb') as chunk_file: + output_file.write(chunk_file.read()) + os.remove(chunk_path) + + if os.path.getsize(save_path) != fileSize: + os.remove(save_path) + return Response(status=500, response="File size mismatch after reassembly") + + except Exception: + for chunk_path in chunk_files: + if os.path.exists(chunk_path): + os.remove(chunk_path) + if os.path.exists(save_path): + os.remove(save_path) + return Response(status=500, response="Error reassembling file") + _launch_scan_video(save_path, config, *_parse_upload_metadata()) return Response(status=201) From 603c406bdd7bc2dcc11eaf92dc995fc2361552f1 Mon Sep 17 00:00:00 2001 From: Shane Israel Date: Thu, 9 Apr 2026 12:54:58 -0600 Subject: [PATCH 2/3] fix files and settings page sidebar not appearing on mobile --- app/client/src/components/nav/Navbar20.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/client/src/components/nav/Navbar20.js b/app/client/src/components/nav/Navbar20.js index 0ce427f0..c459e2d9 100644 --- a/app/client/src/components/nav/Navbar20.js +++ b/app/client/src/components/nav/Navbar20.js @@ -641,7 +641,7 @@ function Navbar20({ ) return ( - {page !== '/login' && page !== '/watch' && page !== '/files' && page !== '/settings' && ( + {page !== '/login' && page !== '/watch' && (isMobile || (page !== '/files' && page !== '/settings')) && ( - {toolbar && page !== '/watch' && page !== '/files' && page !== '/settings' && } + {toolbar && page !== '/watch' && (isMobile || (page !== '/files' && page !== '/settings')) && } setAlert({ ...alert, open })}> {alert.message} From bf7565e767d72433703c56fdaca659fa7085ad5a Mon Sep 17 00:00:00 2001 From: Shane Israel Date: Thu, 9 Apr 2026 17:47:20 -0600 Subject: [PATCH 3/3] mobile updates to file manager --- .../src/components/admin/BulkFileManager.js | 687 ++++++++++-------- 1 file changed, 378 insertions(+), 309 deletions(-) diff --git a/app/client/src/components/admin/BulkFileManager.js b/app/client/src/components/admin/BulkFileManager.js index d749ee10..a0ff436a 100644 --- a/app/client/src/components/admin/BulkFileManager.js +++ b/app/client/src/components/admin/BulkFileManager.js @@ -24,6 +24,8 @@ import { TableContainer, TableHead, TableRow, + useMediaQuery, + useTheme, } from '@mui/material' import DeleteIcon from '@mui/icons-material/Delete' import DriveFileMoveIcon from '@mui/icons-material/DriveFileMove' @@ -43,7 +45,7 @@ import TuneIcon from '@mui/icons-material/Tune' import DeleteSweepIcon from '@mui/icons-material/DeleteSweep' import Select from 'react-select' import selectFolderTheme from '../../common/reactSelectFolderTheme' -import { dialogPaperSx, labelSx } from '../../common/modalStyles' +import { dialogPaperSx, dialogTitleSx, inputSx, labelSx, rowBoxSx } from '../../common/modalStyles' import Api from '../../services/Api' function formatSize(bytes) { @@ -172,6 +174,9 @@ function applyRenameOperation(title, op, find, replace, prefix, suffix) { } export default function BulkFileManager({ setAlert }) { + const theme = useTheme() + const isMobile = useMediaQuery(theme.breakpoints.down('sm')) + const [files, setFiles] = useState([]) const [folders, setFolders] = useState([]) const [loading, setLoading] = useState(true) @@ -552,7 +557,7 @@ export default function BulkFileManager({ setAlert }) { }) } - if (loading) { + if (loading && files.length === 0) { return ( @@ -591,288 +596,400 @@ export default function BulkFileManager({ setAlert }) { borderRadius: '8px', bgcolor: '#1A3A5C', border: '1px solid #3399FF33', - display: 'flex', - alignItems: 'center', - flexWrap: 'wrap', }} > - + {selectedCount} selected - {/* Group 1: Organize */} - - - - - - - - + {isMobile ? ( + /* Mobile: icon-only buttons in a compact wrap row */ + + + { setMoveTargetFolder(null); setMoveModalOpen(true) }} + sx={{ border: '1px solid #3399FF44', borderRadius: 1, color: '#7FBFFF', '&:hover': { bgcolor: '#3399FF12' } }} + > + + + + + { + setRenameOp({ value: 'find_replace', label: 'Find & Replace' }) + setRenameFind(selectedFiles.length === 1 ? (selectedFiles[0].title || selectedFiles[0].filename || '') : '') + setRenameReplace('') + setRenamePrefix('') + setRenameSuffix('') + setRenameDialogOpen(true) + }} + sx={{ border: '1px solid #3399FF44', borderRadius: 1, color: '#7FBFFF', '&:hover': { bgcolor: '#3399FF12' } }} + > + + + + + setRemoveTranscodesDialogOpen(true)} + sx={{ border: '1px solid #FF990044', borderRadius: 1, color: '#FFBB66', '&:hover': { bgcolor: '#FF990012' } }} + > + + + + + setRemoveCropDialogOpen(true)} + sx={{ border: '1px solid #FF990044', borderRadius: 1, color: '#FFBB66', '&:hover': { bgcolor: '#FF990012' } }} + > + + + + + handleSetPrivacy(false)} + disabled={actionLoading} + sx={{ border: '1px solid #1DB95444', borderRadius: 1, color: '#1DB954', '&:hover': { bgcolor: '#1DB95412' } }} + > + + + + + handleSetPrivacy(true)} + disabled={actionLoading} + sx={{ border: '1px solid #FFFFFF33', borderRadius: 1, color: '#FFFFFFCC', '&:hover': { bgcolor: '#FFFFFF0D' } }} + > + + + + + setDeleteDialogOpen(true)} + sx={{ border: '1px solid #f4433644', borderRadius: 1, color: '#f44336', '&:hover': { bgcolor: '#f4433612' } }} + > + + + + + ) : ( + /* Desktop: labelled buttons with dividers */ + + {/* Group 1: Organize */} + + + + + + + + + + + + {/* Group 2: Cleanup */} + + + + + + + + + + + + {/* Group 3: Privacy */} + + + + + + + + + + - + {/* Group 4: Destructive */} + + + + + )} + + )} - {/* Group 2: Cleanup */} - - + {/* ── Search / folder filter / game filter / utility buttons ── */} + {isMobile ? ( + + {/* Row 1: search full width */} + setSearch(e.target.value)} + fullWidth + InputProps={{ + startAdornment: ( + + + + ), + }} + sx={{ ...inputSx, '& .MuiInputBase-input::placeholder': { color: '#FFFFFF55' } }} + /> + {/* Row 2: filters */} + + + ({ value: g, label: g }))]} + value={ + gameFilter === '__all__' + ? { value: '__all__', label: 'All Games' } + : { value: gameFilter, label: gameFilter } + } + onChange={(opt) => setGameFilter(opt.value)} + styles={selectFolderTheme} + isSearchable={false} + /> + + )} + + {/* Row 3: utility buttons */} + + - - + + - - - - - {/* Group 3: Privacy */} - - - + {orphanLoading ? : } + - - + + - - - - {/* Group 4: Destructive */} - - - - )} - - {/* ── Search / folder filter / game filter / create folder ── */} - - setSearch(e.target.value)} - InputProps={{ - startAdornment: ( - - - - ), - }} - sx={{ - flex: 1, - minWidth: 140, - height: 38, - '& .MuiOutlinedInput-root': { - '& fieldset': { borderColor: '#FFFFFF22' }, - '&:hover fieldset': { borderColor: '#FFFFFF44' }, - '&.Mui-focused fieldset': { borderColor: '#FFFFFF66' }, - }, - '& input': { color: '#FFFFFFCC' }, - '& .MuiInputBase-input::placeholder': { color: '#FFFFFF55' }, - }} - /> - - - ({ value: g, label: g }))]} + options={[ + { value: '__all__', label: 'All Folders' }, + ...folders.sort((a, b) => a.localeCompare(b)).map((f) => ({ value: f, label: f })), + ]} value={ - gameFilter === '__all__' - ? { value: '__all__', label: 'All Games' } - : { value: gameFilter, label: gameFilter } + folderFilter === '__all__' + ? { value: '__all__', label: 'All Folders' } + : { value: folderFilter, label: folderFilter } } - onChange={(opt) => setGameFilter(opt.value)} + onChange={(opt) => setFolderFilter(opt.value)} styles={selectFolderTheme} isSearchable={false} /> - )} - - - - - - setColVisAnchor(e.currentTarget)} - sx={{ - border: '1px solid #FFFFFF33', - borderRadius: 1, - color: '#FFFFFFCC', - '&:hover': { borderColor: '#FFFFFF66', bgcolor: '#FFFFFF0D' }, - }} - > - - - + {uniqueGames.length > 0 && ( + +