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
2 changes: 1 addition & 1 deletion app/client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fireshare",
"version": "1.6.13",
"version": "1.6.14",
"private": true,
"dependencies": {
"@emotion/react": "^11.9.0",
Expand Down
2 changes: 1 addition & 1 deletion app/server/fireshare/api/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
8 changes: 4 additions & 4 deletions app/server/fireshare/api/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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']
Expand Down
6 changes: 3 additions & 3 deletions app/server/fireshare/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}")
Expand Down Expand Up @@ -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,
Expand Down
74 changes: 68 additions & 6 deletions app/server/fireshare/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
Loading