Skip to content

Instantly share code, notes, and snippets.

@btTeddy
Forked from tdrnlds/mover.php
Last active March 25, 2023 17:29
Show Gist options
  • Select an option

  • Save btTeddy/0633b8da019b69a55e24561750d74243 to your computer and use it in GitHub Desktop.

Select an option

Save btTeddy/0633b8da019b69a55e24561750d74243 to your computer and use it in GitHub Desktop.
Move directories in which all the MKV and MP4 video files have only one hard link, designed for Unraid
#!/usr/bin/php
#backgroundOnly=true
#arrayStarted=true
#clearLog=true
#noParity=true
# name=tdrnlds custom mover script. Move from cache to disk
# The script will search in cache drive for *.mkv, *.mp4, *.avi, for files with only one hardlink (aka original file) in $source (default: movies).
# If found mixture of files in the same folder e.g *.mkv with hardlink=1 and *.mkv with hardlink=2 folder will be skiped.
# Once files are found it will mark folder containing those files any additional files beside to be moved from $source to $destination until $threshold is acheived.
# $threshold = max disk utilization
# e.g. *.srt, *.nfo and any other files will be moved as well
# Script will start moving oldest files first (modification date).
# Each time after moving a folder it will check if $threshold is achieved.
# It will continue moving folders (containing ONLY *.mkv, *.mp4, *.avi files with single hardlink) untill disk utilisation drops to set &threshold (50% default) or there is no more files to be moved.
# To change file types, just add new extension in this line by adding: <substr( $file, -4 ) === '.extension' ||>
# e.g. to add mp4v file type, change line:
# return substr( $file, -4 ) === '.mkv' || substr( $file, -4 ) === '.mp4' ||substr( $file, -4 ) === '.avi';
# to:
# return substr( $file, -4 ) === '.mkv' || substr( $file, -4 ) === '.mp4' ||substr( $file, -4 ) === '.avi'||substr( $file, -4 ) === '.mp4v';
<?php
# Source = media library location on CACHE drive. Usually the same as plex specific library path e.g /mnt/data/media/movies /mnt/data/media/tv .
# !!! Replace SHARE name <data> with a full CACHE drive name <cache1>. The name can be found in /mnt/
$source = '/mnt/cache1/media/movies';
# Destination = media library location in the array hard drive. Usually the same as plex specific library path e.g /mnt/data/media/movies
# !!! Replace SHARE name <data> with a full DISK name <disk1>. The name can be found in /mnt/
$destination = '/mnt/disk1/media/movies';
# $threshold - disk utilization, once achieved, script will stop
$threshold = 50;
$log = true;
if( ! @file_exists( $source ) ) {
echo "Bad source directory\n";
echo "Usage: ./move.php SOURCE DESTINATION\n";
exit( 1 );
};
if( ! @file_exists( $destination ) ) {
echo "Bad destination directory\n";
echo "Usage: ./move.php SOURCE DESTINATION\n";
exit( 1 );
};
if( parse_ini_file( '/var/local/emhttp/var.ini' )[ 'mdResyncPos' ] ) {
echo "Parity check or rebuild running. Exiting, for safety.\n";
exit( 2 );
};
function getFolderTree( $root, $dir = '' ) {
$tree = [];
foreach( array_diff( scandir( "$root$dir" ), [ '.', '..' ] ) as $name ) {
if( is_dir( "$root$dir/$name" ) )
$tree = array_merge( $tree, getFolderTree( $root, "$dir/$name" ) );
elseif( is_file( "$root$dir/$name" ) )
$tree[] = "$dir/$name";
else {
echo "This is neither a file, nor a directory: $root$dir/$name\n";
exit( 1 );
};
};
return $tree;
};
function getSourceTree( $root, $dir = '' ) {
return array_map( function( $file ) use ( $root ) {
$stat = stat( "$root/$file" );
return [ 'node' => explode( '/', $file )[ 1 ], 'nlink' => $stat[ 'nlink' ], 'mtime' => $stat[ 'mtime' ] ];
}, array_filter( getFolderTree( $root ), function( $file ) {
return substr( $file, -4 ) === '.mkv' || substr( $file, -4 ) === '.mp4' ||substr( $file, -4 ) === '.avi';
} ) );
};
function sortSourceTree( $tree ) {
usort( $tree, function( $a, $b ) {
if( $a[ 'nlink' ] > $b[ 'nlink' ] )
return -1;
if( $a[ 'nlink' ] < $b[ 'nlink' ] )
return 1;
if( $a[ 'mtime' ] > $b[ 'mtime' ] )
return 1;
if( $a[ 'mtime' ] < $b[ 'mtime' ] )
return -1;
return 0;
} );
return $tree;
};
function filterSourceTree( $tree ) {
for( $i = count( $tree ) - 1; $i >= 0; $i-- ) {
for( $j = $i - 1; $j >= 0; $j-- ) {
if( $tree[ $i ][ 'node' ] === $tree[ $j ][ 'node' ] ) {
unset( $tree[ $i ] );
continue 2;
};
};
if( $tree[ $i ][ 'nlink' ] > 1 ) {
unset( $tree[ $i ] );
};
};
return $tree;
};
function usage( $source ) {
return ( disk_total_space( $source ) - disk_free_space( $source ) ) / disk_total_space( $source ) * 100;
};
#=======================================================================================================================
# To get full log comment line # $tree = filterSourceTree( sortSourceTree( getSourceTree( $source ) ) );
$tree = filterSourceTree( sortSourceTree( getSourceTree( $source ) ) );
# Then uncomment these lines beelow:
#=======
# $tree = getSourceTree( $source );
# var_dump( $tree );
# $tree = sortSourceTree( $tree );
# var_dump( $tree );
# $tree = filterSourceTree( $tree );
# var_dump( $tree );
#=======================================================================================================================
while( ( $usage = usage( $source ) ) > $threshold && count( $tree ) > 0 ) {
$next = array_shift( $tree );
if( $log ) echo "Copying: $source/{$next[ 'node' ]} -> $destination/{$next[ 'node' ]}\n";
exec( 'cp -a ' . escapeshellarg( "$source/{$next[ 'node' ]}" ) . ' ' . escapeshellarg( "$destination/{$next[ 'node' ]}" ), $out, $status );
if( $status === 0 ) {
if( $log ) echo "Copy successful. Removing: $source/{$next[ 'node' ]}\n";
exec( 'rm -r ' . escapeshellarg( "$source/{$next[ 'node' ]}" ), $out, $status );
if( $status === 0 ) {
if( $log ) echo "Remove successful. Moving on.\n";
}
else {
if( $log ) echo "Remove failed! Files exist in two places now. Oops. Moving on.\n";
};
}
else {
if( $log ) echo "Copying failed! Skipping remove, and moving on.\n";
};
};
if( $usage > $threshold ) {
if( $log ) echo "Your disk is ", ceil( $usage ), "% full, but there is nothing left to move.\n";
exit( 2 );
};
if( $log ) echo "Your disk is down to ", ceil( $usage ), "% full.\n";
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment