177 lines · 5.6 KB
Raw Download
1
import os
2
import sys
3
from datetime import datetime, timedelta
4
from PIL import Image
5
from PIL.ExifTags import TAGS
6
import shutil
7
8
9
def get_image_datetime(image_path):
10
    """Extract datetime from image metadata"""
11
    try:
12
        img = Image.open(image_path)
13
        exif = img._getexif()
14
        if exif:
15
            for tag, value in exif.items():
16
                if TAGS.get(tag) == "DateTimeOriginal":
17
                    return datetime.strptime(value, "%Y:%m:%d %H:%M:%S")
18
    except (AttributeError, KeyError, ValueError, TypeError):
19
        pass
20
    return None
21
22
23
def find_sequences(files_with_dates, min_sequence_length=50):
24
    """Find sequences of photos taken at regular intervals"""
25
    if not files_with_dates:
26
        return []
27
28
    # Sort files by datetime
29
    sorted_files = sorted(files_with_dates, key=lambda x: x[1])
30
    sequences = []
31
    current_sequence = [sorted_files[0]]
32
33
    for i in range(1, len(sorted_files)):
34
        prev_file, prev_time = sorted_files[i - 1]
35
        curr_file, curr_time = sorted_files[i]
36
        time_diff = curr_time - prev_time
37
38
        if not current_sequence:
39
            current_sequence.append((curr_file, curr_time))
40
            continue
41
42
        # Check if this photo continues the sequence
43
        if len(current_sequence) == 1:
44
            # First interval - can't determine pattern yet
45
            current_sequence.append((curr_file, curr_time))
46
            continue
47
48
        # Calculate expected time based on established interval
49
        first, second = current_sequence[0][1], current_sequence[1][1]
50
        interval = second - first
51
        expected_time = current_sequence[-1][1] + interval
52
        tolerance = timedelta(seconds=interval.total_seconds() * 0.1)  # 10% tolerance
53
54
        if abs(curr_time - expected_time) <= tolerance:
55
            current_sequence.append((curr_file, curr_time))
56
        else:
57
            # Sequence broken
58
            if len(current_sequence) >= min_sequence_length:
59
                sequences.append(current_sequence)
60
            current_sequence = [(curr_file, curr_time)]
61
62
    # Add the last sequence if it's long enough
63
    if len(current_sequence) >= min_sequence_length:
64
        sequences.append(current_sequence)
65
66
    return sequences
67
68
69
def scan_directory(root_dir):
70
    """Scan directory recursively for image files"""
71
    image_extensions = {".jpg", ".jpeg", ".png", ".tiff", ".nef", ".cr2", ".arw"}
72
    files_with_dates = []
73
74
    for dirpath, _, filenames in os.walk(root_dir):
75
        for filename in filenames:
76
            ext = os.path.splitext(filename.lower())[1]
77
            if ext in image_extensions:
78
                filepath = os.path.join(dirpath, filename)
79
                try:
80
                    dt = get_image_datetime(filepath)
81
                    if dt:
82
                        files_with_dates.append((filepath, dt))
83
                except Image.DecompressionBombError:
84
                    print(f"Skipping large image file: {filepath}")
85
                    continue
86
87
    return files_with_dates
88
89
90
def format_sequence_info(sequence):
91
    """Format sequence information for display"""
92
    first_file = sequence[0][0]
93
    last_file = sequence[-1][0]
94
    count = len(sequence)
95
    interval = sequence[1][1] - sequence[0][1]
96
97
    dirname = os.path.dirname(first_file)
98
    first_filename = os.path.basename(first_file)
99
    last_filename = os.path.basename(last_file)
100
101
    return {
102
        "count": count,
103
        "directory": dirname,
104
        "first": first_filename,
105
        "last": last_filename,
106
        "interval": interval,
107
    }
108
109
110
def move_sequence(sequence, base_dir):
111
    """Move sequence to its own folder"""
112
    if not sequence:
113
        return
114
115
    first_file = sequence[0][0]
116
    dirname = os.path.dirname(first_file)
117
    seq_start = sequence[0][1].strftime("%Y%m%d_%H%M%S")
118
    seq_end = sequence[-1][1].strftime("%H%M%S")
119
    new_dirname = os.path.join(base_dir, f"timelapse_{seq_start}-{seq_end}")
120
121
    os.makedirs(new_dirname, exist_ok=True)
122
123
    for filepath, _ in sequence:
124
        filename = os.path.basename(filepath)
125
        new_path = os.path.join(new_dirname, filename)
126
        shutil.move(filepath, new_path)
127
128
    return new_dirname
129
130
131
def main():
132
    if len(sys.argv) < 2:
133
        print("Usage: python timelapse_detector.py <directory>")
134
        return
135
136
    root_dir = sys.argv[1]
137
    print(f"Scanning {root_dir} for timelapse sequences...")
138
139
    files_with_dates = scan_directory(root_dir)
140
    sequences = find_sequences(files_with_dates)
141
142
    if not sequences:
143
        print("No timelapse sequences found.")
144
        return
145
146
    print(f"\nFound {len(sequences)} timelapse sequences:")
147
    sequence_info = []
148
    for i, seq in enumerate(sequences, 1):
149
        info = format_sequence_info(seq)
150
        sequence_info.append(info)
151
        print(f"\nSequence {i}:")
152
        print(f"  Photos: {info['count']}")
153
        print(f"  Directory: {info['directory']}")
154
        print(f"  First file: {info['first']}")
155
        print(f"  Last file: {info['last']}")
156
        print(
157
            f"  Interval: ~{info['interval'].total_seconds():.1f} seconds between photos"
158
        )
159
160
    response = (
161
        input("\nWould you like to move these sequences to their own folders? (y/n): ")
162
        .strip()
163
        .lower()
164
    )
165
    if response == "y":
166
        base_dir = os.path.abspath(root_dir)
167
        for i, seq in enumerate(sequences, 1):
168
            new_dir = move_sequence(seq, base_dir)
169
            print(f"Moved sequence {i} to {new_dir}")
170
        print("\nDone moving sequences.")
171
    else:
172
        print("No files were moved.")
173
174
175
if __name__ == "__main__":
176
    main()
177