Compare commits
17 Commits
ff824ad3c0
...
main
Author | SHA1 | Date | |
---|---|---|---|
6be8ece8d4
|
|||
770d3cd25b | |||
658addae56
|
|||
79cd7a32ed
|
|||
02279b8c6f
|
|||
ed3c6d7cc7
|
|||
bc5371de71
|
|||
2afecd1c04
|
|||
8fbe5ae3c4
|
|||
acf7b5047f
|
|||
82d02cfe76
|
|||
d19ab90f38
|
|||
477e8951a9
|
|||
5110f41152 | |||
2de5044dfe
|
|||
9d4f4319c2
|
|||
ca24665574
|
5
.dockerignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.git
|
||||||
|
.env
|
||||||
|
metadata
|
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
metadata
|
7
Dockerfile
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
FROM python:3.13.3-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["python3.13", "src/server.py"]
|
1
editor/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
metadata/
|
|
@ -55,7 +55,6 @@ def encode(input_file, codec, remove_source=False, save_log=False):
|
|||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
"-i", input_file,
|
"-i", input_file,
|
||||||
"-map", "0",
|
"-map", "0",
|
||||||
"-cq", str(cq),
|
|
||||||
] + extra_params + [
|
] + extra_params + [
|
||||||
"-c:v", ffmpeg_codec,
|
"-c:v", ffmpeg_codec,
|
||||||
"-preset", "p4",
|
"-preset", "p4",
|
@ -55,8 +55,6 @@ def get_video_metadata(file_path):
|
|||||||
"channels": stream.get("channels", 0),
|
"channels": stream.get("channels", 0),
|
||||||
"flags": {
|
"flags": {
|
||||||
"default": stream.get("disposition", {}).get("default", 0) == 1,
|
"default": stream.get("disposition", {}).get("default", 0) == 1,
|
||||||
"forced": stream.get("disposition", {}).get("forced", 0) == 1,
|
|
||||||
"hearing_impaired": stream.get("disposition", {}).get("hearing_impaired", 0) == 1,
|
|
||||||
"visual_impaired": stream.get("disposition", {}).get("visual_impaired", 0) == 1,
|
"visual_impaired": stream.get("disposition", {}).get("visual_impaired", 0) == 1,
|
||||||
"original": stream.get("disposition", {}).get("original", 0) == 1,
|
"original": stream.get("disposition", {}).get("original", 0) == 1,
|
||||||
"commentary": stream.get("disposition", {}).get("comment", 0) == 1
|
"commentary": stream.get("disposition", {}).get("comment", 0) == 1
|
||||||
@ -73,7 +71,6 @@ def get_video_metadata(file_path):
|
|||||||
"default": stream.get("disposition", {}).get("default", 0) == 1,
|
"default": stream.get("disposition", {}).get("default", 0) == 1,
|
||||||
"forced": stream.get("disposition", {}).get("forced", 0) == 1,
|
"forced": stream.get("disposition", {}).get("forced", 0) == 1,
|
||||||
"hearing_impaired": stream.get("disposition", {}).get("hearing_impaired", 0) == 1,
|
"hearing_impaired": stream.get("disposition", {}).get("hearing_impaired", 0) == 1,
|
||||||
"visual_impaired": stream.get("disposition", {}).get("visual_impaired", 0) == 1,
|
|
||||||
"original": stream.get("disposition", {}).get("original", 0) == 1,
|
"original": stream.get("disposition", {}).get("original", 0) == 1,
|
||||||
"commentary": stream.get("disposition", {}).get("comment", 0) == 1
|
"commentary": stream.get("disposition", {}).get("comment", 0) == 1
|
||||||
}
|
}
|
||||||
@ -86,13 +83,13 @@ def get_video_metadata(file_path):
|
|||||||
print(f"❌ Error processing {file_path}: {str(e)}")
|
print(f"❌ Error processing {file_path}: {str(e)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def process_file(file_path, output_file=None):
|
def process_file(file_path, output_dir=None):
|
||||||
"""
|
"""
|
||||||
Process a single video file and write metadata to JSON.
|
Process a single video file and write metadata to JSON.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_path (str): Path to the video file
|
file_path (str): Path to the video file
|
||||||
output_file (str, optional): Path to output JSON file
|
output_dir (str, optional): Directory where the output JSON file will be saved
|
||||||
"""
|
"""
|
||||||
if not os.path.isfile(file_path):
|
if not os.path.isfile(file_path):
|
||||||
print(f"❌ File not found: {file_path}")
|
print(f"❌ File not found: {file_path}")
|
||||||
@ -106,27 +103,34 @@ def process_file(file_path, output_file=None):
|
|||||||
metadata = get_video_metadata(file_path)
|
metadata = get_video_metadata(file_path)
|
||||||
|
|
||||||
if metadata:
|
if metadata:
|
||||||
if not output_file:
|
# Generate output filename based on input file
|
||||||
# Generate output filename based on input file
|
filename = os.path.basename(os.path.splitext(file_path)[0]) + "_metadata.json"
|
||||||
|
|
||||||
|
if output_dir:
|
||||||
|
# Ensure output directory exists
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
output_path = os.path.join(output_dir, filename)
|
||||||
|
else:
|
||||||
|
# If no output directory specified, save in the same directory as the input file
|
||||||
base_name = os.path.splitext(file_path)[0]
|
base_name = os.path.splitext(file_path)[0]
|
||||||
output_file = f"{base_name}_metadata.json"
|
output_path = f"{base_name}_metadata.json"
|
||||||
|
|
||||||
# Write metadata to JSON file
|
# Write metadata to JSON file
|
||||||
with open(output_file, 'w', encoding='utf-8') as f:
|
with open(output_path, 'w', encoding='utf-8') as f:
|
||||||
json.dump(metadata, f, indent=2, ensure_ascii=False)
|
json.dump(metadata, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
print(f"✅ Metadata saved to {output_file}")
|
print(f"✅ Metadata saved to {output_path}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def process_directory(directory_path, output_file=None):
|
def process_directory(directory_path, output_dir=None):
|
||||||
"""
|
"""
|
||||||
Process all video files in a directory and write metadata to JSON.
|
Process all video files in a directory and write metadata to JSON.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
directory_path (str): Path to the directory
|
directory_path (str): Path to the directory
|
||||||
output_file (str, optional): Path to output JSON file
|
output_dir (str, optional): Directory where the output JSON file will be saved
|
||||||
"""
|
"""
|
||||||
if not os.path.isdir(directory_path):
|
if not os.path.isdir(directory_path):
|
||||||
print(f"❌ Directory not found: {directory_path}")
|
print(f"❌ Directory not found: {directory_path}")
|
||||||
@ -152,31 +156,38 @@ def process_directory(directory_path, output_file=None):
|
|||||||
print(f"❌ No supported video files found in {directory_path}")
|
print(f"❌ No supported video files found in {directory_path}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not output_file:
|
# Generate output filename based on directory name
|
||||||
# Generate output filename based on directory name
|
dir_name = os.path.basename(os.path.normpath(directory_path))
|
||||||
dir_name = os.path.basename(os.path.normpath(directory_path))
|
filename = f"{dir_name}_metadata.json"
|
||||||
output_file = f"{dir_name}_metadata.json"
|
|
||||||
|
if output_dir:
|
||||||
|
# Ensure output directory exists
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
output_path = os.path.join(output_dir, filename)
|
||||||
|
else:
|
||||||
|
# If no output directory specified, save in the current directory
|
||||||
|
output_path = filename
|
||||||
|
|
||||||
# Write all metadata to a single JSON file
|
# Write all metadata to a single JSON file
|
||||||
with open(output_file, 'w', encoding='utf-8') as f:
|
with open(output_path, 'w', encoding='utf-8') as f:
|
||||||
json.dump(all_metadata, f, indent=2, ensure_ascii=False)
|
json.dump(all_metadata, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
print(f"✅ Metadata for {file_count} files saved to {output_file}")
|
print(f"✅ Metadata for {file_count} files saved to {output_path}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Extract metadata from video files and save as JSON.")
|
parser = argparse.ArgumentParser(description="Extract metadata from video files and save as JSON.")
|
||||||
parser.add_argument("input", help="Path to input video file or directory")
|
parser.add_argument("input", help="Path to input video file or directory")
|
||||||
parser.add_argument("-o", "--output", help="Path to output JSON file")
|
parser.add_argument("-o", "--output", help="Directory path where output JSON files will be saved")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
input_path = args.input
|
input_path = args.input
|
||||||
output_file = args.output
|
output_dir = args.output
|
||||||
|
|
||||||
if os.path.isfile(input_path):
|
if os.path.isfile(input_path):
|
||||||
process_file(input_path, output_file)
|
process_file(input_path, output_dir)
|
||||||
elif os.path.isdir(input_path):
|
elif os.path.isdir(input_path):
|
||||||
process_directory(input_path, output_file)
|
process_directory(input_path, output_dir)
|
||||||
else:
|
else:
|
||||||
print(f"❌ Path not found: {input_path}")
|
print(f"❌ Path not found: {input_path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
128
src/public/static/images/collection.svg
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
viewBox="0 0 128 128"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||||
|
sodipodi:docname="collection.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="5.6568543"
|
||||||
|
inkscape:cx="69.384853"
|
||||||
|
inkscape:cy="46.050329"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1016"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="27"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="g8-2">
|
||||||
|
<inkscape:grid
|
||||||
|
id="grid1"
|
||||||
|
units="px"
|
||||||
|
originx="0"
|
||||||
|
originy="0"
|
||||||
|
spacingx="1"
|
||||||
|
spacingy="0.99999998"
|
||||||
|
empcolor="#0099e5"
|
||||||
|
empopacity="0.30196078"
|
||||||
|
color="#0099e5"
|
||||||
|
opacity="0.14901961"
|
||||||
|
empspacing="16"
|
||||||
|
enabled="true"
|
||||||
|
visible="true" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect10"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,8.0000005,0,1 @ F,0,0,1,0,8.0000005,0,1 @ F,0,0,1,0,8.0000005,0,1 @ F,0,0,1,0,8.0000005,0,1 @ F,0,0,1,0,8.0000005,0,1 @ F,0,1,1,0,5,0,1 @ F,0,1,1,0,5,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect9"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,1,1,0,0,0,1 @ F,0,0,1,0,8.0000004,0,1 @ F,0,1,1,0,8.0000004,0,1 @ F,0,1,1,0,0,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect8"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,1,1,0,5,0,1 @ F,0,0,1,0,8,0,1 @ F,0,0,1,0,8,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,8,0,1 @ F,0,1,1,0,5,0,1 @ F,0,1,1,0,5,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" />
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
id="g8-2"
|
||||||
|
transform="rotate(7.9490233,62.107327,266.90842)">
|
||||||
|
<g
|
||||||
|
id="g10"
|
||||||
|
transform="translate(-12.771862,-9.814157)">
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:1.99937;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 83.459997,59.049888 82.353661,51.126757 C 81.742649,46.750932 77.700018,43.698948 73.324193,44.30996 L 38.66049,49.15018 C 35.925599,49.532063 33.398955,47.624573 33.017073,44.889682 32.39989,42.466468 29.93516,41.002392 27.511947,41.619576 L 8.6945086,44.247124 C 4.3186836,44.858137 1.2667,48.900767 1.8777122,53.276592 L 9.760356,109.72891 c 0.611016,4.37582 4.653645,7.4278 9.029468,6.81679 l 63.385056,-8.85068 c 3.853961,-0.53814 6.681016,-3.7382 6.883076,-7.48335"
|
||||||
|
id="path8"
|
||||||
|
sodipodi:nodetypes="ccccccsccccc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:1.99937;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 9.6220641,108.73852 11.643471,76.990746 a 9.8076021,9.8076021 132.84706 0 1 8.431469,-9.09017 l 63.385056,-8.850688 a 6.5830057,6.5830057 42.600843 0 1 7.483432,6.881572 l -1.886976,34.28013"
|
||||||
|
id="path9"
|
||||||
|
sodipodi:nodetypes="cccc" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |