|
|
|
|
@ -1,116 +1,118 @@
|
|
|
|
|
from watchdog.events import FileSystemEventHandler
|
|
|
|
|
from watchdog.observers import Observer
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import shutil
|
|
|
|
|
import time
|
|
|
|
|
import os
|
|
|
|
|
from funcs import get_media_dimensions
|
|
|
|
|
|
|
|
|
|
media_dir = 'media'
|
|
|
|
|
stories_dir = 'stories'
|
|
|
|
|
posts_dir = 'posts'
|
|
|
|
|
# Base directories
|
|
|
|
|
media_dir = Path("media")
|
|
|
|
|
stories_dir = media_dir / "stories"
|
|
|
|
|
posts_dir = media_dir / "posts"
|
|
|
|
|
|
|
|
|
|
# Ensure output dirs exist
|
|
|
|
|
stories_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
posts_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def wait_for_complete(file_path, timeout=10):
|
|
|
|
|
"""
|
|
|
|
|
Wait until a file is fully written (size stops changing).
|
|
|
|
|
Returns True if stable within timeout, else False.
|
|
|
|
|
"""
|
|
|
|
|
file_path = Path(file_path)
|
|
|
|
|
prev_size = -1
|
|
|
|
|
for _ in range(timeout * 2): # check every 0.5 sec
|
|
|
|
|
try:
|
|
|
|
|
size = file_path.stat().st_size
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
return False
|
|
|
|
|
if size == prev_size:
|
|
|
|
|
return True
|
|
|
|
|
prev_size = size
|
|
|
|
|
time.sleep(0.5)
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_story(width, height, tolerance=0.02):
|
|
|
|
|
"""
|
|
|
|
|
Determine if the given width/height are close to 9:16 (0.5625) ratio
|
|
|
|
|
within a certain tolerance. Default tolerance is 2%.
|
|
|
|
|
|
|
|
|
|
Tolerance means how close the ratio must be to 9/16 for it
|
|
|
|
|
to be considered a story.
|
|
|
|
|
Check if dimensions are close to 9:16 (0.5625) ratio.
|
|
|
|
|
"""
|
|
|
|
|
if width == 0 or height == 0:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# Calculate the ratio in portrait orientation (ensure width < height).
|
|
|
|
|
# You can also just do width/height, but watch out for landscape images.
|
|
|
|
|
# We’ll assume portrait means stories.
|
|
|
|
|
ratio = width / height if width < height else height / width
|
|
|
|
|
|
|
|
|
|
# The official story ratio is 9/16 = 0.5625
|
|
|
|
|
story_ratio = 9/16
|
|
|
|
|
# Check how far off we are from the official ratio
|
|
|
|
|
difference = abs(ratio - story_ratio)
|
|
|
|
|
|
|
|
|
|
# If the difference is within the tolerance, we consider it a story
|
|
|
|
|
return difference <= (story_ratio * tolerance)
|
|
|
|
|
ratio = min(width, height) / max(width, height)
|
|
|
|
|
return abs(ratio - (9 / 16)) <= (9 / 16 * tolerance)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def determine_post_type(filepath):
|
|
|
|
|
"""
|
|
|
|
|
Determines if a file is for 'posts' or 'stories' based on its aspect ratio.
|
|
|
|
|
- If the path includes 'posts' (as you mentioned), we automatically return 'posts'.
|
|
|
|
|
- Otherwise, we check if the aspect ratio matches (roughly) the 9:16 ratio.
|
|
|
|
|
- If it does, we say 'stories', otherwise 'posts'.
|
|
|
|
|
Determines if a file is 'posts' or 'stories' based on aspect ratio.
|
|
|
|
|
"""
|
|
|
|
|
# If "posts" is part of the filepath, consider it a post
|
|
|
|
|
if 'posts' in filepath.lower():
|
|
|
|
|
return 'posts'
|
|
|
|
|
|
|
|
|
|
# Get actual dimensions
|
|
|
|
|
lower = str(filepath).lower()
|
|
|
|
|
if "posts" in lower:
|
|
|
|
|
return "posts"
|
|
|
|
|
try:
|
|
|
|
|
width, height = get_media_dimensions(filepath)
|
|
|
|
|
except:
|
|
|
|
|
# If we fail to get dimensions, return None or some fallback
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# If dimensions are invalid, return None or False
|
|
|
|
|
if width == 0 or height == 0:
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Error getting dimensions for {filepath}: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# Use our ratio check
|
|
|
|
|
if is_story(width, height):
|
|
|
|
|
return 'stories'
|
|
|
|
|
else:
|
|
|
|
|
return 'posts'
|
|
|
|
|
return "stories" if is_story(width, height) else "posts"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DownloadHandler(FileSystemEventHandler):
|
|
|
|
|
def process_file(self, file_path):
|
|
|
|
|
file = os.path.basename(file_path)
|
|
|
|
|
|
|
|
|
|
if 'crdownload' in file:
|
|
|
|
|
file_path = Path(file_path)
|
|
|
|
|
file = file_path.name
|
|
|
|
|
|
|
|
|
|
# Skip temp/incomplete files
|
|
|
|
|
if "crdownload" in file or file.count("~") != 3:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if file.count('~') != 3:
|
|
|
|
|
if not file_path.exists():
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if not os.path.exists(file_path):
|
|
|
|
|
if not wait_for_complete(file_path):
|
|
|
|
|
print(f"File {file_path} did not stabilize. Skipping.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
post_type = determine_post_type(file_path)
|
|
|
|
|
if post_type == 'posts':
|
|
|
|
|
media_type_dir = posts_dir
|
|
|
|
|
elif post_type == 'stories':
|
|
|
|
|
media_type_dir = stories_dir
|
|
|
|
|
if post_type == "posts":
|
|
|
|
|
dest_dir = posts_dir
|
|
|
|
|
elif post_type == "stories":
|
|
|
|
|
dest_dir = stories_dir
|
|
|
|
|
else:
|
|
|
|
|
print(f"Could not determine post type for {file}. Skipping...")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
outputPath = os.path.join(media_dir, media_type_dir, file)
|
|
|
|
|
output_path = dest_dir / file
|
|
|
|
|
|
|
|
|
|
if os.path.exists(outputPath):
|
|
|
|
|
print(f"File already exists {outputPath}. Removing...")
|
|
|
|
|
os.remove(file_path)
|
|
|
|
|
if output_path.exists():
|
|
|
|
|
print(f"File already exists {output_path}. Removing...")
|
|
|
|
|
file_path.unlink(missing_ok=True)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
shutil.move(file_path, outputPath)
|
|
|
|
|
print(f"Moved {file_path} to {outputPath}")
|
|
|
|
|
shutil.move(str(file_path), str(output_path))
|
|
|
|
|
print(f"Moved {file_path} → {output_path}")
|
|
|
|
|
|
|
|
|
|
def on_created(self, event):
|
|
|
|
|
if not event.is_directory and 'crdownload' not in event.src_path:
|
|
|
|
|
if not event.is_directory:
|
|
|
|
|
self.process_file(event.src_path)
|
|
|
|
|
|
|
|
|
|
def on_moved(self, event):
|
|
|
|
|
if not event.is_directory and 'crdownload' not in event.dest_path:
|
|
|
|
|
if not event.is_directory:
|
|
|
|
|
self.process_file(event.dest_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
downloadPath = os.path.join(os.path.expanduser('~'), 'Downloads')
|
|
|
|
|
download_path = Path.home() / "Downloads"
|
|
|
|
|
event_handler = DownloadHandler()
|
|
|
|
|
observer = Observer()
|
|
|
|
|
observer.schedule(event_handler, downloadPath, recursive=False)
|
|
|
|
|
observer.schedule(event_handler, str(download_path), recursive=False)
|
|
|
|
|
observer.start()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
while True:
|
|
|
|
|
time.sleep(1) # Add a 1-second sleep to reduce CPU usage
|
|
|
|
|
observer.join() # idle until interrupted
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
observer.stop()
|
|
|
|
|
observer.join()
|
|
|
|
|
observer.join()
|
|
|
|
|
|