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
128 changes: 127 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,127 @@
# MotionExtract
# MotionExtract

A command-line tool to split Google Motion Photos into separate photo and video files.

## What are Motion Photos?

Motion Photos (on Google Pixel phones) store a short video clip inside a JPG file. This tool extracts them into separate files.

## Usage

### Process a folder

```bash
# Windows
MotionExtract "C:\Users\YourName\Pictures\MotionPhotos"

# Linux/macOS
./MotionExtract "/home/user/Pictures/MotionPhotos"
```

### Specify output directory

```bash
# Use --output or -o to specify where to save extracted files
MotionExtract "C:\Photos" --output "D:\Extracted"

# Short form
MotionExtract "C:\Photos" -o "D:\Extracted"

# Without --output, files are saved to <source-directory>/output (default)
```

### Interactive mode

```bash
# Run without arguments to be prompted for a directory
MotionExtract
```

### Get help

```bash
MotionExtract --help
# or
MotionExtract -h
```

### Check version

```bash
MotionExtract --version
# or
MotionExtract -v
```

### Output

Files are saved to an `output` subdirectory:

- `<filename>_photo.jpg` - The still photo
- `<filename>_video.mp4` - The video clip

**Example:**

```text
Input: C:\Photos\PXL_20220613_003727701.MP.jpg
Output: C:\Photos\output\PXL_20220613_003727701.MP_photo.jpg
C:\Photos\output\PXL_20220613_003727701.MP_video.mp4
```

### Sample Output

```text
Scanning for files in: C:\Photos
Found 15 file(s)
Output directory: C:\Photos\output

[1/15] IMG_1234.bmp... ⚠ Skipped (not JPG)
[2/15] PXL_20220613_003727701.MP.jpg... ✓
[3/15] PXL_20220614_120000123.MP.jpg... ✓
...

Summary:
Total processed: 15
Extracted: 12
Skipped: 3
```

### Exit Codes

- `0` - Success (all files processed without errors)
- `1` - Errors occurred during processing

## Requirements

- .NET 8.0 SDK or runtime

## Installation

```bash
# Build
dotnet build -c Release

# Executable will be in: src/MotionExtract/bin/Release/net8.0/
```

## Developer Commands

```bash
# Run tests
dotnet test

# Run application
dotnet run --project src/MotionExtract -- "C:\path\to\photos"

# Format code
dotnet format

# Clean
dotnet clean
```

## How It Works

Searches for the MP4 `ftyp` signature and JPG end marker (`FF D9`) to split the embedded files.

Based on [this solution](https://android.stackexchange.com/a/203898).
1 change: 1 addition & 0 deletions src/MotionExtract/IPhotoVideo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ public interface IPhotoVideo
{
void Extract();
void Save(string outputDir);
bool HasValidData();
}
1 change: 1 addition & 0 deletions src/MotionExtract/MotionExtract.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>1.0.0</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.ILLink.Tasks" Version="8.0.20" />
Expand Down
18 changes: 8 additions & 10 deletions src/MotionExtract/MotionPhoto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ public class MotionPhoto(FileInfo baseFile) : IPhotoVideo

public void Save(string outputDir)
{
// Ensure output directory exists
Directory.CreateDirectory(outputDir);

var baseFileName = Path.GetFileNameWithoutExtension(_baseFile.FullName);

var jpgFileName = $"{baseFileName}_photo.jpg";
Expand All @@ -21,8 +24,6 @@ public void Save(string outputDir)

public void Extract()
{
Console.WriteLine($"Processing: {_baseFile.FullName}, size: {_baseFile.Length} bytes");

var data = File.ReadAllBytes(_baseFile.FullName);

// Look for the position of the "ftyp" in the data to detect MP4 start
Expand All @@ -42,17 +43,14 @@ public void Extract()
JpgData = [.. data.Take(jpgEndPos)];
Mp4Data = [.. data.Skip(mp4StartPos)];
}
else
{
Console.WriteLine("SKIPPING - File appears to contain an MP4 but no valid JPG EOI segment could be found.");
}
}
else
{
Console.WriteLine("SKIPPING - File does not appear to be a Google motion photo.");
}
}

public bool HasValidData()
{
return JpgData.Length > 0 && Mp4Data.Length > 0;
}

/// <summary>
/// Find the position of the "ftyp" pattern in the byte array
/// </summary>
Expand Down
Loading