Skip to content

Instantly share code, notes, and snippets.

@Eun
Created June 12, 2025 14:52
Show Gist options
  • Save Eun/4dad712bfd3efa2b791abe19e1db48e6 to your computer and use it in GitHub Desktop.
Save Eun/4dad712bfd3efa2b791abe19e1db48e6 to your computer and use it in GitHub Desktop.
detect resolution of video files and add the resolution in the end of of the file.
#!/usr/bin/env perl
use strict;
use warnings;
use Getopt::Long;
use File::Basename;
use JSON;
# Resolution mapping - maps actual resolutions to category labels
my %RESOLUTION_MAP = (
# 2160p (4K) variants
'3840x2160' => '2160p',
'4096x2160' => '2160p',
'3840x1604' => '2160p',
'3840x1600' => '2160p',
'4096x1714' => '2160p',
# 1440p variants (treat as 2160p for our purposes, or add separate category)
'2560x1440' => '1440p',
'2560x1080' => '1440p',
# 1080p variants
'1920x1080' => '1080p',
'1920x800' => '1080p',
'1920x804' => '1080p',
'1920x816' => '1080p',
'1920x960' => '1080p',
'1920x1040' => '1080p',
'1800x1080' => '1080p',
'1858x1080' => '1080p',
# 720p variants
'1280x720' => '720p',
'1280x534' => '720p',
'1280x536' => '720p',
'1280x544' => '720p',
'1280x694' => '720p',
'1366x768' => '720p',
# 480p variants
'854x480' => '480p',
'720x480' => '480p',
'640x480' => '480p',
'848x480' => '480p',
'852x480' => '480p',
# 360p variants
'640x360' => '360p',
'480x360' => '360p',
'638x360' => '360p',
);
# Fallback resolution detection based on height
my %HEIGHT_MAP = (
2160 => '2160p',
1440 => '1440p',
1080 => '1080p',
720 => '720p',
480 => '480p',
360 => '360p',
);
# Valid resolution categories (excluding 1440p from your original list)
my @VALID_RESOLUTIONS = ('2160p', '1080p', '720p', '480p', '360p');
# Command line options
my $dry_run = 0;
my $replace_existing = 0;
my $help = 0;
GetOptions(
'dry-run|n' => \$dry_run,
'replace|r' => \$replace_existing,
'help|h' => \$help,
) or die "Error in command line arguments\n";
if ($help || @ARGV == 0) {
print_usage();
exit 0;
}
sub print_usage {
print <<'EOF';
Usage: $0 [OPTIONS] FILE [FILE...]
Rename video files to include resolution in square brackets.
Options:
-n, --dry-run Show what would be renamed without actually renaming
-r, --replace Replace existing resolution tags instead of skipping
-h, --help Show this help message
Examples:
$0 movie.mkv
$0 --dry-run *.mp4
$0 --replace --dry-run video1.mkv video2.avi
Supported resolutions: 2160p, 1080p, 720p, 480p, 360p
EOF
}
sub get_video_resolution {
my ($file) = @_;
# Use ffprobe to get video stream information in JSON format
my $cmd = qq{ffprobe -v quiet -print_format json -show_streams "$file" 2>/dev/null};
my $output = `$cmd`;
return undef unless $output && $? == 0;
my $data;
eval {
$data = decode_json($output);
};
return undef if $@;
# Find the first video stream
for my $stream (@{$data->{streams} || []}) {
next unless $stream->{codec_type} && $stream->{codec_type} eq 'video';
my $width = $stream->{width};
my $height = $stream->{height};
next unless defined $width && defined $height;
my $resolution_key = "${width}x${height}";
# Check exact resolution mapping first
if (exists $RESOLUTION_MAP{$resolution_key}) {
my $category = $RESOLUTION_MAP{$resolution_key};
# Only return if it's in our valid list
return $category if grep { $_ eq $category } @VALID_RESOLUTIONS;
}
# Fallback to height-based detection
if (exists $HEIGHT_MAP{$height}) {
my $category = $HEIGHT_MAP{$height};
return $category if grep { $_ eq $category } @VALID_RESOLUTIONS;
}
# Try to match height within reasonable range
for my $target_height (sort { $b <=> $a } keys %HEIGHT_MAP) {
if (abs($height - $target_height) <= 50) { # Allow 50px variance
my $category = $HEIGHT_MAP{$target_height};
return $category if grep { $_ eq $category } @VALID_RESOLUTIONS;
}
}
return undef; # Resolution not supported
}
return undef; # No video stream found
}
sub extract_existing_resolution {
my ($filename) = @_;
# Look for pattern like " - [1080p]" or " [720p]" at the end of filename
if ($filename =~ /^(.+?)(?:\s*-\s*)?\[(\d+p)\](\.[^.]+)$/) {
return ($1, $2, $3); # (base_name, resolution, extension)
}
return (undef, undef, undef);
}
sub rename_file {
my ($original_file, $resolution) = @_;
my ($name, $path, $suffix) = fileparse($original_file, qr/\.[^.]*/);
# Check if file already has resolution tag
my ($base_name, $existing_res, $ext) = extract_existing_resolution($name . $suffix);
if (defined $existing_res) {
if (!$replace_existing) {
print "SKIP: $original_file (already has resolution tag [$existing_res])\n";
return 'skipped';
}
# Use the base name without existing resolution
$name = $base_name;
$suffix = $ext;
}
my $new_name = "${name} - [${resolution}]${suffix}";
my $new_file = $path . $new_name;
if ($original_file eq $new_file) {
print "SKIP: $original_file (is already correct named)\n";
return 'skipped';
}
if ($dry_run) {
print "DRY-RUN: $original_file -> $new_name\n";
return 'renamed';
} else {
if (rename($original_file, $new_file)) {
print "RENAMED: $original_file -> $new_name\n";
return 'renamed';
} else {
warn "ERROR: Failed to rename $original_file: $!\n";
return 'error';
}
}
}
# Process each file
my $renamed_count = 0;
my $error_count = 0;
my $skipped_count = 0;
my $multiple_files = @ARGV > 1;
for my $file (@ARGV) {
unless (-f $file) {
warn "ERROR: File not found: $file\n";
$error_count++;
next;
}
print "Processing: $file\n" if $dry_run;
my $resolution = get_video_resolution($file);
unless (defined $resolution) {
warn "ERROR: Could not determine resolution for: $file\n";
$error_count++;
next;
}
my $result = rename_file($file, $resolution);
if ($result eq 'renamed') {
$renamed_count++;
} elsif ($result eq 'skipped') {
$skipped_count++;
} elsif ($result eq 'error') {
$error_count++;
}
}
# Print summary only when multiple files were processed
if ($multiple_files) {
print "\n--- SUMMARY ---\n";
print "Files renamed: $renamed_count\n";
print "Files skipped: $skipped_count\n";
print "Files errored: $error_count\n";
print "Total files processed: " . ($renamed_count + $skipped_count + $error_count) . "\n";
}
if ($error_count > 0) {
print STDERR "\nCompleted with $error_count error(s)\n";
exit 1;
}
exit 0;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment