Files
Plex-Export/cli.php
2010-11-03 21:16:44 +00:00

548 lines
15 KiB
PHP

<?php
/*
Plex Export
Luke Lanchester <luke@lukelanchester.com>
A CLI script to export information from your Plex library.
Usage:
php cli.php [-plex-url="http://your-plex-library:32400"] [-data-dir="plex-data"] [-sections=1,2,3 or "Movies,TV Shows"]
*/
$timer_start = microtime(true);
$plex_export_version = 1;
ini_set('memory_limit', '128M');
set_error_handler('plex_error_handler');
error_reporting(E_ALL ^ E_NOTICE | E_WARNING);
plex_log('Welcome to the Plex Exporter v'.$plex_export_version);
// Set-up
$defaults = array(
'plex-url' => 'http://localhost:32400',
'data-dir' => 'plex-data',
'thumbnail-width' => 150,
'sections' => 'all'
);
$options = hl_parse_arguments($_SERVER['argv'], $defaults);
if(substr($options['plex-url'],-1)!='/') $options['plex-url'] .= '/'; // Always have a trailing slash
$options['absolute-data-dir'] = dirname(__FILE__).'/'.$options['data-dir']; // Run in current dir (PHP CLI defect)
check_dependancies(); // Check everything is enabled as necessary
// Load details about all sections
$all_sections = load_all_sections();
if(!$all_sections) {
plex_error('Could not load section data, aborting');
exit();
}
if($options['sections'] == 'all') {
$sections = $all_sections;
} else {
$sections_to_show = array_filter(explode(',',$options['sections']));
$section_titles = array();
foreach($all_sections as $i=>$section) $section_titles[strtolower($section['title'])] = $i;
foreach($sections_to_show as $section_key_or_title) {
$section_title = strtolower(trim($section_key_or_title));
if(array_key_exists($section_title, $section_titles)) {
$section_id = $section_titles[$section_title];
$sections[$section_id] = $all_sections[$section_id];
continue;
}
$section_id = intval($section_key_or_title);
if(array_key_exists($section_id, $all_sections)) {
$sections[$section_id] = $all_sections[$section_id];
continue;
}
plex_error('Could not find section: '.$section_key_or_title);
} // end foreach: $sections_to_show
} // end if: !all sections
$num_sections = count($sections);
if($num_sections==0) {
plex_error('No sections were found to scan');
exit();
}
// Load details about each section
$total_items = 0;
$section_display_order = array();
foreach($sections as $i=>$section) {
plex_log('Scanning section: '.$section['title']);
$items = load_items_for_section($section);
if(!$items) {
plex_error('No items were added for '.$section['title'].', skipping');
$sections[$i]['num_items'] = 0;
$sections[$i]['items'] = array();
continue;
}
$num_items = count($items);
$total_items += $num_items;
plex_log('Analysing media items in section...');
$sorts_title = $sorts_release = $sorts_rating = array();
$raw_section_genres = array();
foreach($items as $key=>$item) {
$sorts_title[$key] = (substr(strtolower($item['title']),0,4)=='the ')?substr($item['title'],4):$item['title'];
$sorts_release[$key] = @strtotime($item['release_date']);
$sorts_rating[$key] = ($item['user_rating'])?$item['user_rating']:$item['rating'];
if(is_array($item['genre']) and count($item['genre'])>0) {
foreach($item['genre'] as $genre) {
$raw_section_genres[$genre]++;
}
}
}
asort($sorts_title, SORT_STRING);
asort($sorts_release, SORT_NUMERIC);
asort($sorts_rating, SORT_NUMERIC);
$sorts['title_asc'] = array_keys($sorts_title);
$sorts['release_asc'] = array_keys($sorts_release);
$sorts['rating_asc'] = array_keys($sorts_rating);
$sorts['title_desc'] = array_reverse($sorts['title_asc']);
$sorts['release_desc'] = array_reverse($sorts['release_asc']);
$sorts['rating_desc'] = array_reverse($sorts['rating_asc']);
$section_genres = array();
if(count($raw_section_genres)>0) {
arsort($raw_section_genres);
foreach($raw_section_genres as $genre=>$genre_count) {
$section_genres[] = array(
'genre' => $genre,
'count' => $genre_count,
);
}
}
$section_display_order[] = $i;
$sections[$i]['num_items'] = $num_items;
$sections[$i]['items'] = $items;
$sections[$i]['sorts'] = $sorts;
$sections[$i]['genres'] = $section_genres;
plex_log('Added '.$num_items.' '.hl_inflect($num_items,'item').' from the '.$section['title'].' section');
} // end foreach: $sections_to_export
// Output all data
plex_log('Exporting data for '.$num_sections.' '.hl_inflect($num_sections,'section').' containing '.$total_items.' '.hl_inflect($total_items,'item'));
$output = array(
'status' => 'success',
'version' => $plex_export_version,
'last_generated' => time()*1000,
'total_items' => $total_items,
'num_sections' => $num_sections,
'section_display_order' => $section_display_order,
'sections' => $sections
);
$output = json_encode($output);
$filename = $options['absolute-data-dir'].'/data.js';
$bytes_written = file_put_contents($filename, $output);
if(!$bytes_written) {
plex_error('Could not save JSON data to '.$filename.', please make sure directory is writeable');
exit();
}
plex_log('Wrote '.$bytes_written.' bytes to '.$filename);
$timer_end = microtime(true);
$time_taken = $timer_end - $timer_start;
plex_log('Plex Export completed in '.round($time_taken,2).' seconds');
// Methods
function load_items_for_section($section) {
global $options;
$url = $options['plex-url'].'library/sections/'.$section['key'].'/all';
$xml = load_xml_from_url($url);
if(!$xml) return false;
$num_items = intval($xml->attributes()->size);
if($num_items<=0) {
plex_error('No items were found in this section, skipping');
return false;
}
switch($section['type']) {
case 'movie':
$object_to_loop = $xml->Video;
$object_parser = 'load_data_for_movie';
break;
case 'show':
$object_to_loop = $xml->Directory;
$object_parser = 'load_data_for_show';
break;
default:
plex_error('Unknown section type provided to parse: '.$section['type']);
return false;
}
plex_log('Found '.$num_items.' '.hl_inflect($num_items,$section['type']).' in '.$section['title']);
$items = array();
foreach($object_to_loop as $el) {
$item = $object_parser($el);
if($item) $items[$item['key']] = $item;
}
return $items;
} // end func: load_items_for_section
function load_data_for_movie($el) {
global $options;
$_el = $el->attributes();
$key = intval($_el->ratingKey);
if($key<=0) return false;
$title = strval($_el->title);
plex_log('Scanning movie: '.$title);
$thumb = generate_item_thumbnail(strval($_el->thumb), $key, $title);
$item = array(
'key' => $key,
'type' => 'movie',
'thumb' => $thumb,
'title' => $title,
'duration' => floatval($_el->duration),
'view_count' => intval($_el->viewCount),
'tagline' => ($_el->tagline)?strval($_el->tagline):false,
'rating' => ($_el->rating)?floatval($_el->rating):false,
'user_rating' => ($_el->userRating)?floatval($_el->userRating):false,
'release_year' => ($_el->year)?intval($_el->year):false,
'release_date' => ($_el->originallyAvailableAt)?strval($_el->originallyAvailableAt):false,
'content_rating' => ($_el->contentRating)?strval($_el->contentRating):false,
'summary' => ($_el->summary)?strval($_el->summary):false,
'studio' => ($_el->studio)?strval($_el->studio):false,
'genre' => false,
'director' => false,
'role' => false,
'media' => false,
);
$media_el = $el->Media->attributes();
if(intval($media_el->duration)>0) {
$item['media'] = array(
'bitrate' => ($media_el->bitrate)?intval($media_el->bitrate):false,
'aspect_ratio' => ($media_el->aspectRatio)?floatval($media_el->aspectRatio):false,
'audio_channels' => ($media_el->audioChannels)?intval($media_el->audioChannels):false,
'audio_codec' => ($media_el->audioCodec)?strval($media_el->audioCodec):false,
'video_codec' => ($media_el->videoCodec)?strval($media_el->videoCodec):false,
'video_resolution' => ($media_el->videoResolution)?intval($media_el->videoResolution):false,
'video_framerate' => ($media_el->videoFrameRate)?strval($media_el->videoFrameRate):false,
'total_size' => false
);
$total_size = 0;
foreach($el->Media->Part as $part) {
$total_size += floatval($part->attributes()->size);
}
if($total_size>0) {
$item['media']['total_size'] = $total_size;
}
}
$url = $options['plex-url'].'library/metadata/'.$key;
$xml = load_xml_from_url($url);
if(!$xml) {
plex_error('Could not load additional metadata for '.$title);
return $item;
}
$genres = array();
foreach($xml->Video->Genre as $genre) $genres[] = strval($genre->attributes()->tag);
if(count($genres)>0) $item['genre'] = $genres;
$directors = array();
foreach($xml->Video->Director as $director) $directors[] = strval($director->attributes()->tag);
if(count($directors)>0) $item['director'] = $directors;
$roles = array();
foreach($xml->Video->Role as $role) $roles[] = strval($role->attributes()->tag);
if(count($roles)>0) $item['role'] = $roles;
return $item;
} // end func: load_data_for_movie
function load_data_for_show($el) {
global $options;
$_el = $el->attributes();
$key = intval($_el->ratingKey);
if($key<=0) return false;
$title = strval($_el->title);
plex_log('Scanning show: '.$title);
$thumb = generate_item_thumbnail(strval($_el->thumb), $key, $title);
$item = array(
'key' => $key,
'type' => 'movie',
'thumb' => $thumb,
'title' => $title,
'rating' => ($_el->rating)?floatval($_el->rating):false,
'user_rating' => ($_el->userRating)?floatval($_el->userRating):false,
'release_year' => ($_el->year)?intval($_el->year):false,
'release_date' => ($_el->originallyAvailableAt)?strval($_el->originallyAvailableAt):false,
'duration' => floatval($_el->duration),
'content_rating' => ($_el->contentRating)?strval($_el->contentRating):false,
'summary' => ($_el->summary)?strval($_el->summary):false,
'studio' => ($_el->studio)?strval($_el->studio):false,
'tagline' => false,
'num_episodes' => intval($_el->leafCount),
'num_seasons' => false,
'seasons' => array()
);
$genres = array();
foreach($el->Genre as $genre) $genres[] = strval($genre->attributes()->tag);
if(count($genres)>0) $item['genre'] = $genres;
$url = $options['plex-url'].'library/metadata/'.$key.'/children';
$xml = load_xml_from_url($url);
if(!$xml) {
plex_error('Could not load additional metadata for '.$title);
return $item;
}
$seasons = array();
foreach($xml->Directory as $el2) {
if($el2->attributes()->type!='season') continue;
$season_key = intval($el2->attributes()->ratingKey);
$season = array(
'key' => $season_key,
'title' => strval($el2->attributes()->title),
'num_episodes' => intval($el2->attributes()->leafCount),
'index' => intval($el2->attributes()->index)
);
$seasons[$season_key] = $season;
}
$item['num_seasons'] = count($seasons);
if($item['num_seasons']>0) $item['seasons'] = $seasons;
return $item;
} // end func: load_data_for_show
function load_all_sections() {
global $options;
$url = $options['plex-url'].'library/sections';
plex_log('Searching for sections in the Plex library at '.$options['plex-url']);
$xml = load_xml_from_url($url);
if(!$xml) return false;
$total_sections = intval($xml->attributes()->size);
if($total_sections<=0) {
plex_error('No sections were found in this Plex library');
return false;
}
$sections = array();
$num_sections = 0;
foreach($xml->Directory as $el) {
$_el = $el->attributes();
$key = intval($_el->key);
$type = strval($_el->type);
$title = strval($_el->title);
if($type=='movie' or $type=='show') {
$sections[$key] = array('key'=>$key, 'type'=>$type, 'title'=>$title);
$num_sections++;
} else {
plex_error('Skipping section of unknown type: '.$type);
}
}
if($num_sections==0) {
plex_error('No valid sections found, aborting');
return false;
}
if($total_sections!=$num_sections) {
plex_log('Found '.$num_sections.' valid '.hl_inflect($num_sections, 'section').' out of a possible '.$total_sections.' '.hl_inflect($total_sections, 'section').' in this Plex library');
} else {
plex_log('Found '.$num_sections.' '.hl_inflect($num_sections, 'section').' in this Plex library');
}
return $sections;
} // end func: load_all_sections
function load_xml_from_url($url) {
global $options;
if(!@fopen($url, 'r')) {
plex_error('The Plex library could not be found at '.$options['plex-url']);
return false;
}
$xml = @simplexml_load_file($url);
if(!$xml) {
plex_error('Data could not be read from the Plex server at '.$url);
return false;
}
if(!$xml) {
plex_error('Invalid XML returned by the Plex server, aborting');
return false;
}
return $xml;
} // end func: load_xml_from_url
function generate_item_thumbnail($thumb_url, $key, $title) {
global $options;
$filename = '/thumb_'.$key.'.png';
$save_filename = $options['absolute-data-dir'].$filename;
$return_filename = $options['data-dir'].$filename;
if(file_exists($save_filename)) return $return_filename;
if($thumb_url=='') {
plex_error('No thumbnail URL was provided for '.$title, ', skipping');
return false;
}
$source = $options['plex-url'].substr($thumb_url,1);
$img_data = @file_get_contents($source);
if(!$img_data) {
plex_error('Could not load thumbnail for '.$title,' skipping');
return false;
}
$im = imagecreatefromstring($img_data);
$width = imagesx($im);
if($width > $options['thumbnail-width']) {
$height = imagesy($im);
$scale = $width / $options['thumbnail-width'];
$new_width = $options['thumbnail-width'];
$new_height = $height / $scale;
$old_image = $im;
$im = imagecreatetruecolor($new_width, $new_height);
imagecopyresampled($im, $old_image, 0, 0, 0, 0, $new_width, $new_height, $width, $height);
imagedestroy($old_image);
}
imagepng($im, $save_filename);
imagedestroy($im);
unset($img_data);
return $return_filename;
} // end func: generate_item_thumbnail
function plex_log($str) {
$str = @date('H:i:s')." $str\n";
fwrite(STDOUT, $str);
} // end func: plex_log
function plex_error($str) {
$str = @date('H:i:s')." Error: $str\n";
fwrite(STDERR, $str);
} // end func: plex_error
function plex_error_handler($errno, $errstr, $errfile=null, $errline=null) {
if(!(error_reporting() & $errno)) return;
$str = @date('H:i:s')." Error: $errstr". ($errline?' on line '.$errline:'') ."\n";
fwrite(STDERR, $str);
} // end func: plex_error_handler
function check_dependancies() {
global $options;
$errors = false;
if(!extension_loaded('simplexml')) {
plex_error('SimpleXML is not enabled');
$errors = true;
}
if(!extension_loaded('gd')) {
plex_error('GD is not enabled');
$errors = true;
}
if(!ini_get('allow_url_fopen')) {
plex_error('Remote URL access is disabled (allow_url_fopen)');
$errors = true;
}
if(!is_writable($options['absolute-data-dir'])) {
plex_error('Data directory is not writeable at '.$options['absolute-data-dir']);
$errors = true;
}
if($errors) {
plex_error('Failed one or more dependancy checks; aborting');
exit();
}
} // end func: check_dependancies
function hl_parse_arguments($cli_args, $defaults) {
$output = (array) $defaults;
foreach($cli_args as $str) {
if(substr($str,0,1)!='-') continue;
$eq_pos = strpos($str, '=');
$key = substr($str, 1, $eq_pos-1);
if(!array_key_exists($key, $output)) continue;
$output[$key] = substr($str, $eq_pos+1);
}
return $output;
} // end func: hl_parse_arguments
function hl_inflect($num, $single, $plural=false) {
if($num==1) return $single;
if($plural) return $plural;
return $single.'s';
} // end func: hl_inflect