diff --git a/app/client/package-lock.json b/app/client/package-lock.json index 7814921e..f5412109 100644 --- a/app/client/package-lock.json +++ b/app/client/package-lock.json @@ -1,6 +1,6 @@ { "name": "fireshare", - "version": "1.6.13", + "version": "1.6.14", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/app/client/package.json b/app/client/package.json index 80cfe62c..785da8e8 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -1,6 +1,6 @@ { "name": "fireshare", - "version": "1.6.13", + "version": "1.6.14", "private": true, "dependencies": { "@emotion/react": "^11.9.0", diff --git a/app/server/fireshare/api/helpers.py b/app/server/fireshare/api/helpers.py index 830fc59a..5c1c753a 100644 --- a/app/server/fireshare/api/helpers.py +++ b/app/server/fireshare/api/helpers.py @@ -8,7 +8,7 @@ def secure_filename(filename): - clean = re.sub(r"[/\\?%*:|\"<>\x7F\x00-\x1F]", "-", filename) + clean = re.sub(r"[/\\?%*:|\"<>\x7F\x00-\x1F\s]", "-", filename) return clean diff --git a/app/server/fireshare/api/upload.py b/app/server/fireshare/api/upload.py index 8e24f77f..4385d1e1 100644 --- a/app/server/fireshare/api/upload.py +++ b/app/server/fireshare/api/upload.py @@ -101,7 +101,7 @@ def public_upload_video(): upload_folder = config['app_config']['public_upload_folder_name'] if config['app_config'].get('allow_public_folder_selection', False): requested_folder = request.form.get('folder', '').strip() - if requested_folder and '/' not in requested_folder and '..' not in requested_folder: + if requested_folder and '/' not in requested_folder and '..' not in requested_folder and ' ' not in requested_folder: upload_folder = requested_folder if 'file' not in request.files: @@ -174,7 +174,7 @@ def public_upload_videoChunked(): if config['app_config'].get('allow_public_folder_selection', False): requested_folder = request.form.get('folder', '').strip() - if requested_folder and '/' not in requested_folder and '..' not in requested_folder: + if requested_folder and '/' not in requested_folder and '..' not in requested_folder and ' ' not in requested_folder: upload_folder = requested_folder upload_directory = paths['video'] / upload_folder @@ -297,7 +297,7 @@ def upload_video(): upload_folder = config['app_config']['admin_upload_folder_name'] requested_folder = request.form.get('folder', '').strip() - if requested_folder and '/' not in requested_folder and '..' not in requested_folder: + if requested_folder and '/' not in requested_folder and '..' not in requested_folder and ' ' not in requested_folder: upload_folder = requested_folder if 'file' not in request.files: @@ -341,7 +341,7 @@ def upload_videoChunked(): upload_folder = config['app_config']['admin_upload_folder_name'] requested_folder = request.form.get('folder', '').strip() - if requested_folder and '/' not in requested_folder and '..' not in requested_folder: + if requested_folder and '/' not in requested_folder and '..' not in requested_folder and ' ' not in requested_folder: upload_folder = requested_folder required_files = ['blob'] diff --git a/app/server/fireshare/cli.py b/app/server/fireshare/cli.py index 6a3dfd5b..6b317cbd 100755 --- a/app/server/fireshare/cli.py +++ b/app/server/fireshare/cli.py @@ -677,7 +677,7 @@ def create_posters(regenerate, skip): if should_create_poster: if not derived_path.exists(): derived_path.mkdir(parents=True) - poster_time = int(vi.duration * skip) + poster_time = int((vi.duration or 0) * skip) util.create_poster(video_path, derived_path / "poster.jpg", poster_time) else: logger.debug(f"Skipping creation of poster for video {vi.video_id} because it exists at {str(poster_path)}") @@ -1182,8 +1182,8 @@ def scan_image(ctx, path, game_id, tag_ids, title): else: logger.debug(f"Image {iid} already indexed") else: - created_at = datetime.fromtimestamp(os.path.getmtime(str(img_file))) - updated_at = datetime.fromtimestamp(os.path.getmtime(str(img_file))) + created_at = util.extract_date_from_image_file(img_file) + updated_at = created_at source_folder = rel_path.split('/')[0] if '/' in rel_path else None img = Image(image_id=iid, extension=img_file.suffix, path=rel_path, available=True, created_at=created_at, updated_at=updated_at, diff --git a/app/server/fireshare/util.py b/app/server/fireshare/util.py index 53d5c890..7265b978 100755 --- a/app/server/fireshare/util.py +++ b/app/server/fireshare/util.py @@ -175,9 +175,9 @@ def video_id(path: Path, mb=16): def get_media_info(path): try: - cmd = f'ffprobe -v quiet -print_format json -show_entries stream {path}' - logger.debug(f"$ {cmd}") - data = json.loads(sp.check_output(cmd.split()).decode('utf-8')) + cmd = ['ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_entries', 'stream', str(path)] + logger.debug(f"$ {' '.join(cmd)}") + data = json.loads(sp.check_output(cmd).decode('utf-8')) return data['streams'] except Exception as ex: logger.warning('Could not extract video info') @@ -1361,9 +1361,9 @@ def _extract_date_from_metadata(file_path: Path): datetime object if a valid date was found in metadata, None otherwise """ try: - cmd = f'ffprobe -v quiet -print_format json -show_entries format_tags {file_path}' - logger.debug(f"$ {cmd}") - data = json.loads(sp.check_output(cmd.split()).decode('utf-8')) + cmd = ['ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_entries', 'format_tags', str(file_path)] + logger.debug(f"$ {' '.join(cmd)}") + data = json.loads(sp.check_output(cmd).decode('utf-8')) tags = data.get('format', {}).get('tags', {}) if not tags: @@ -1405,6 +1405,68 @@ def _extract_date_from_metadata(file_path: Path): return None +def _extract_date_from_image_exif(file_path: Path): + """ + Extract creation date from image EXIF metadata using PIL. + + Checks DateTimeOriginal (36867), DateTimeDigitized (36868), and DateTime (306). + """ + try: + from PIL import Image as PILImage + with PILImage.open(str(file_path)) as img: + exif_data = img._getexif() + if not exif_data: + return None + # EXIF tag IDs for date fields in priority order + for tag_id in (36867, 36868, 306): + value = exif_data.get(tag_id) + if value: + try: + parsed = datetime.strptime(value, '%Y:%m:%d %H:%M:%S') + if 2000 <= parsed.year <= datetime.now().year + 1: + logger.debug(f"Extracted date {parsed} from image EXIF tag {tag_id}") + return parsed + except ValueError: + continue + return None + except Exception as ex: + logger.debug(f"Failed to extract EXIF date from image: {ex}") + return None + + +def extract_date_from_image_file(file_path: Path): + """ + Extract a creation date from an image file. + + Tries in order: + 1. EXIF metadata (DateTimeOriginal, DateTimeDigitized, DateTime) + 2. Filename date patterns + 3. File modification time + """ + file_path = Path(file_path) + + if not file_path.exists(): + logger.warning(f"Cannot extract date from non-existent file: {file_path}") + return None + + exif_date = _extract_date_from_image_exif(file_path) + if exif_date: + return exif_date + + filename_date = _extract_date_from_filename(file_path.name) + if filename_date: + logger.debug(f"Using filename date for {file_path.name}: {filename_date}") + return filename_date + + try: + created_date = datetime.fromtimestamp(os.path.getmtime(file_path)) + logger.debug(f"Using file mtime for {file_path.name}: {created_date}") + return created_date + except Exception as ex: + logger.warning(f"Failed to get file mtime for {file_path}: {ex}") + return None + + def extract_date_from_file(file_path: Path): """ Extract a recording date from a video file.