I recently moved all my movies and only changed the Plex docker host path. But not the Plex docker path, which remained the same at: ‘/movies’
As a result, all the movies ‘date added’ dates changed to the date I moved my all the movies (used mv cmd) and no longer has the date the movies were actually added to my library.
I want to address this by updating the date added on all my movies to match the media file date created date.
I’m considering a PHP script like this to accomplish this. Has anyone done something like this or can provide guidance?
<?php
/**
* Plex Media Timestamp Updater
* Updates the added_at timestamp in Plex database to match file creation time
*
* IMPORTANT: Stop Plex Media Server before running this script!
*/
// Configuration
$filepath = "."; // Path to your movie files
$plexdb = "/path/to/your/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db";
$dryRun = true; // Set to false to actually update the database
// Validate paths
if (!is_dir($filepath)) {
exit('ERROR: File path does not exist: ' . $filepath . "\n");
}
if (!is_file($plexdb)) {
exit('ERROR: Plex database does not exist: ' . $plexdb . "\n");
}
// Display mode
if ($dryRun) {
echo "========================================\n";
echo " DRY RUN MODE - NO CHANGES\n";
echo "========================================\n";
echo "This will simulate the process without modifying the database.\n";
echo "Set \$dryRun = false to perform actual updates.\n\n";
} else {
echo "========================================\n";
echo " LIVE UPDATE MODE\n";
echo "========================================\n";
echo "WARNING: This will modify your Plex database!\n";
echo "Make sure Plex Media Server is STOPPED before continuing!\n";
echo "Press Enter to continue or Ctrl+C to cancel...\n";
fgets(STDIN);
}
// Backup Plex database (only if not dry run)
if (!$dryRun) {
$backupPath = $plexdb . '-backup-' . time();
if (copy($plexdb, $backupPath)) {
echo "Database backed up to: $backupPath\n\n";
} else {
exit("ERROR: Failed to backup database\n");
}
} else {
echo "Dry run mode: Skipping database backup\n\n";
}
// Open database (read-only for dry run)
try {
if ($dryRun) {
$db = new SQLite3($plexdb, SQLITE3_OPEN_READONLY);
} else {
$db = new SQLite3($plexdb);
}
} catch (Exception $e) {
exit('ERROR: Could not open database: ' . $e->getMessage() . "\n");
}
// Enable error handling
$db->enableExceptions(true);
$processedCount = 0;
$errorCount = 0;
// Scan directory
$files = scandir($filepath);
if ($files === false) {
$db->close();
exit("ERROR: Could not read directory: $filepath\n");
}
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
$fileFullPath = $filepath . DIRECTORY_SEPARATOR . $file;
// Skip directories
if (!is_file($fileFullPath)) {
continue;
}
// Get file creation time
// Note: filectime() returns creation time on Windows, inode change time on Unix/Linux
// For true creation time on all platforms, you may need additional extensions
$fileCreateTime = filectime($fileFullPath);
if ($fileCreateTime === false) {
echo "Processing $file\n\tERROR: Could not get file time\n\n";
$errorCount++;
continue;
}
echo "Processing $file\n";
echo "\tFile creation/change time: " . date('Y-m-d H:i:s', $fileCreateTime) . "\n";
try {
// Escape the filename for safe SQL usage
$escapedFile = SQLite3::escapeString($file);
// Find media_item_id by file path
// Using LIKE with directory separator to better match full paths
$query = "SELECT media_item_id FROM media_parts WHERE file LIKE '%" . SQLite3::escapeString(DIRECTORY_SEPARATOR) . "$escapedFile'";
$result = $db->querySingle($query, true);
if (!$result) {
echo "\tmedia_item_id not found in Plex database.\n\n";
$errorCount++;
continue;
}
$media_item_id = $result['media_item_id'];
echo "\tFound media_item_id: $media_item_id\n";
// Find metadata_item_id by media_item_id
$escapedMediaId = SQLite3::escapeString($media_item_id);
$result = $db->querySingle("SELECT metadata_item_id FROM media_items WHERE id = '$escapedMediaId'", true);
if (!$result) {
echo "\tmetadata_item_id not found.\n\n";
$errorCount++;
continue;
}
$metadata_item_id = $result['metadata_item_id'];
echo "\tFound metadata_item_id: $metadata_item_id\n";
// Get current added_at value
$escapedMetadataId = SQLite3::escapeString($metadata_item_id);
$currentResult = $db->querySingle("SELECT added_at FROM metadata_items WHERE id = '$escapedMetadataId'", true);
if ($currentResult && isset($currentResult['added_at'])) {
$currentTimestamp = $currentResult['added_at'];
echo "\tCurrent added_at: " . date('Y-m-d H:i:s', $currentTimestamp) . "\n";
echo "\tWould update to: " . date('Y-m-d H:i:s', $fileCreateTime) . "\n";
$timeDiff = abs($fileCreateTime - $currentTimestamp);
if ($timeDiff > 0) {
echo "\tTime difference: " . round($timeDiff / 86400, 1) . " days\n";
} else {
echo "\tNo change needed (timestamps match)\n";
}
}
if ($dryRun) {
echo "\t[DRY RUN] Would update added_at timestamp.\n\n";
$processedCount++;
} else {
// Update added_at field to file creation time (as Unix epoch)
$updateQuery = "UPDATE metadata_items SET added_at = $fileCreateTime WHERE id = '$escapedMetadataId'";
$updateResult = $db->exec($updateQuery);
if ($updateResult === false) {
echo "\tERROR: Failed to update - " . $db->lastErrorMsg() . "\n\n";
$errorCount++;
} else {
echo "\tSuccessfully updated added_at timestamp.\n\n";
$processedCount++;
}
}
} catch (Exception $e) {
echo "\tERROR: " . $e->getMessage() . "\n\n";
$errorCount++;
}
}
$db->close();
// Summary
echo "===========================================\n";
echo "Processing complete!\n";
if ($dryRun) {
echo "Mode: DRY RUN (no changes made)\n";
echo "Files that would be updated: $processedCount\n";
} else {
echo "Mode: LIVE UPDATE\n";
echo "Successfully updated: $processedCount files\n";
}
echo "Errors encountered: $errorCount files\n";
echo "===========================================\n";
if ($dryRun) {
echo "\nTo perform actual updates, set \$dryRun = false in the script.\n";
} else {
echo "\nREMEMBER: Restart Plex Media Server and refresh your library.\n";
}
?>
It’s worth noting, Jellyfin was not affected by the “date added” timestamp change because, unlike Plex, Jellyfin generally tracks media using its internal file database and inode information rather than relying solely on the host path mapping of the Docker container.
Any help validating this script or a better approach to fix this inplex would be greatly appreciated.