web-dev-qa-db-fra.com

Afficher (et gérer) des images de webcam?

J'ai une webcam qui écrit toutes les minutes ou à peu près, une image dans un dossier FTP sur le serveur où WP est installé. Ma question est de savoir comment afficher la dernière image sur la page, avec une actualisation toutes les 60 secondes, et supprimer les anciennes dans ce dossier.

4
Valentino Dell'Aica

Très bien, voici le code permettant de redessiner une image d’une page sur une page au fur et à mesure que de nouvelles images sont détectées (comme pour un répertoire dans lequel une webcam envoie automatiquement des images). Il suppose ce qui suit:

  1. Vous utilisez PHP 5.3 ou supérieur (facilement modifié pour ne pas en avoir besoin)
  2. Vous avez vos images Web cam télécharger quelque part accessible directement via le Web
  3. Vous ne voulez pas mettre plusieurs images de cam sur une seule page ou post

Je suis sûr qu'il y a encore beaucoup à faire, mais il a les effets suivants:

  • Permet de configurer différentes pages de cam

La section 1 est le fichier de plugin principal. Il suffit de le placer dans un fichier php, avec un en-tête de plugin, et de le placer à la racine du dossier du plugin approprié.

La section 2 est le fichier javascript. Le fichier de plug-in principal suppose qu'il se trouve dans un sous-répertoire nommé js et porte le nom de fichier web_cam_checker.js.

Section 1: Le fichier PHP

/*
Plugin Name: Whatever You Want Here
Description: Handles updating an image via the heartbeat api for a web cam - in answer to a question on stack exchange
Version: 0.0.1
Author: The Privateer
*/
namespace Privateer\WebCam;

try {
    if ( class_exists('Privateer_Do_Web_Cam_Updates') ) {
        throw new \Exception('Privateer_Do_Web_Cam_Updates already exists!', 10001);
    } else {

        class Privateer_Do_Web_Cam_Updates{

            protected $images_dir;              # file system url images are uploaded to
            protected $images_url;              # web uri images are available at
            protected $image_tag_id;            # id of image to be swapped out on displayed page
            protected $do_purge_images;     # should old files be deleted
            protected $refresh_interval_s;  # how often the cam should refresh
            protected $is_debug;                    # boolean - use debug mode?
            protected $init_retry_ms;           # Time in seconds to wait for initialization each try
            protected $min_init_retries;        # Maximum number of attempts to wait for initialization before quitting

            protected $notices;                 # Any notices issued
            protected $errors;                  # Any errors

            function __construct(
                $image_tag_id = '',
                $images_dir = '',
                $images_url = '',
                $refresh_interval_s = 0,
                $init_retry_ms = 0,
                $min_init_retries = 0,
                $is_debug = false,
                $do_purge_images = false
            ) {

                $this->notices = array();


                $defaults = $this->get_default_settings();

                $this->images_dir = ( empty($images_dir) )? $defaults['images_dir'] : (string) $images_dir;
                $this->validate_images_dir_or_throw();

                $images_url = ( empty($images_url) )? $defaults['images_url'] : (string) $images_url;
                if ( empty( $images_url ) ) {
                    throw new \Exception("URL [{$images_url}] not found. Use _privateer_web_cam_images_url filter to set properly.", 10001);
                } else {
                    $this->images_url = $images_url;
                }

                $image_tag_id = ( empty($image_tag_id) ) ? $defaults['image_tag_id'] : (string) $image_tag_id;
                if ( empty($image_tag_id) ) {
                    throw new \Exception("Image Tag ID empty. Please fix via _privateer_web_cam_image_tag_id filter.", 10001);
                } else {
                    $this->image_tag_id = $image_tag_id;
                }

                $do_purge_images = ( empty($do_purge_images) ) ? $defaults['purge_old_images'] : (bool) $do_purge_images;
                $this->do_purge_images = ( $do_purge_images === true )? true : false;

                # Limitations imposed by wp.heartbeat
                $refresh_interval_s = ( empty( $refresh_interval_s ) )? $defaults['refresh_interval_seconds'] : (int) $refresh_interval_s;
                if ( 5 > $refresh_interval_s ) {
                    $this->notices[] = "Min Refresh Interval is 5 seconds. Adjusted from {$refresh_interval_s} to 5.";
                    $this->refresh_interval_s = 5;
                } else if ( 120 < $refresh_interval_s ) {
                    $this->notices[] = "Max Refresh Interval is 120 seconds. Adjusted from {$refresh_interval_s} to 120.";
                    $this->refresh_interval_s = 120;
                } else {
                    $this->refresh_interval_s = $refresh_interval_s;
                }

                $is_debug = ( is_null($is_debug) )? $defaults['debug'] : (bool) $is_debug;
                $this->is_debug = ( $is_debug )? 1 : 0;

                $init_retry_ms = ( empty( $init_retry_ms ) )? $defaults['init_retry_ms'] : (int) $init_retry_ms;
                if ( 200 > $init_retry_ms ) {
                    $this->notices[] = "Init Retry Time mimimum is 200 milliseconds. Adjusted from {$init_retry_ms} to 200.";
                    $this->init_retry_ms = 200;
                } else {
                    $this->init_retry_ms = $init_retry_ms;
                }

                $min_init_retries = ( empty( $min_init_retries ) )? $defaults['init_min_retries'] : (int) $min_init_retries;
                if ( 1 > $min_init_retries ) {
                    $this->notices[] = "Min Init Retries is 1. Adjusted from {$min_init_retries} to 1.";
                    $this->min_init_retries = 1;
                } else {
                    $this->min_init_retries = $min_init_retries;
                }

            }

            protected function get_default_settings() {
                return array(
                    'images_dir' => plugin_dir_path( __FILE__ ) . 'cam-images',
                    'images_url' => plugin_dir_url( __FILE__ ) . 'cam-images',
                    'image_tag_id' => 'main_cam_image',
                    'purge_old_images' => false,
                    'refresh_interval_seconds' => 30,
                    'debug' => WP_DEBUG,
                    'init_retry_ms' => 500,
                    'init_min_retries' => 10
                );
            }

            protected function validate_images_dir_or_throw() {
                if ( !is_dir( $this->images_dir ) ) {
                    throw new \Exception("Directory [{$this->images_dir}] not found. Use _privateer_web_cam_images_dir filter to set properly.", 10001);
                } else if ( !is_readable( $this->images_dir) ) {
                    throw new \Exception("Directory [{$this->images_dir}] not readable.", 10001);
                }
            }

            # The function that processes received heartbeats via ajax
            # - response: what we will be sending back (filtered)
            # - data: what we received
            # - screen_id: will be 'front' or an admin page
            # Anything returning an error key will tell the javascript to stop
            public function do_process_heartbeat_received( $response, $data, $screen_id ) {
                $r = array();

                $key = 'web_cam_checker_' . $this->image_tag_id;

                if ( 'front' !== "{$screen_id}" ) {
                    $r['error'] = 'Not on front end of site.';
                } else if ( !array_key_exists($key, $data) ) {
                    $r['error'] = "Failed to locate key {$key} in data received";
                } else if ( !array_key_exists('current_image_src', $data["{$key}"]) ) {
                    $r['error'] = "Did not find current_image_src in {$key} data";
                } else {
                    $current = $this->get_current_web_cam_image();
                    $reported = (string) $data["{$key}"]['current_image_src'];
                    if ( "{$current}" == "{$reported}" ) {
                        $r['notice'] = 'Image has not changed';
                    } else {
                        $r['webcam_new_uri'] = "{$current}";
                    }
                }
                $response["{$key}"] = $r;

                return $response;
            }

            protected function get_readable_images_in_image_dir() {

                $this->validate_images_dir_or_throw();

                $images = array();

                if ( $handle = opendir( "{$this->images_dir}" ) ) {
                    while ( false !== ( $file_name = readdir( $handle ) ) ) {
                        switch ( "{$file_name}" ) {
                            case '.':
                            case '..':
                                # Skip current and previous directory links
                                break;
                            default:
                                # Build the full file path to the file found
                                $file_path = "{$this->images_dir}/{$file_name}";
                                if ( is_file( "{$file_path}" ) && is_readable( "{$file_path}" ) ) {
                                    # TODO: Check to be sure it is an image
                                    $images["{$file_name}"] = $file_path;
                                }
                                break;
                        }
                    }
                    @closedir( $handle );
                } else {
                    $this->notices[] = "Failed to open directory {$this->images_dir} for reading.";
                }

                return $images;
            }

            protected function get_newest_image_name($images) {
                $newest_name = '';
                $newest_ts = 0;
                foreach ( $images as $name => $path ) {
                    $last_modified = filectime( $path );
                    if ( $last_modified > $newest_ts ) {
                        $newest_name = $name;
                        $newest_ts = $last_modified;
                    }
                }
                return $newest_name;
            }

            protected function get_current_web_cam_image() {
                $current = '';              # The newest image on the web server

                try {
                    $this->validate_images_dir_or_throw();

                    $images = $this->get_readable_images_in_image_dir();
                    if ( 0 < count($images) ) {
                        $newest_name = $this->get_newest_image_name($images);

                        $current = "{$this->images_url}/{$newest_name}";

                        if ( $this->do_purge_images ) {
                            $this->purge_older_images($images, $newest_name);
                        }
                    }
                } catch ( \Exception $e ) {
                    $this->append_exception( $e );

                    $code = $e->getCode();
                    $message = $e->getMessage();
                    $trace = $e->getTraceAsString();
                    $line = $e->getLine();
                    $file = $e->getFile();
                    $err = new \WP_Error( "Error: {$file}(line {$line}): {$code} {$message}", $trace );

                    # You can hook into this to log errors somewhere if wanted
                    do_action('_privateer_do_web_cam_updates_error', $err);
                }
                return $current;
            }

            protected function append_exception( \Exception $e ) {
                $code = $e->getCode();
                $message = $e->getMessage();
                $trace = $e->getTraceAsString();
                $line = $e->getLine();
                $file = $e->getFile();

                $this->errors[] = new \WP_Error( "Error: {$file}(line {$line}): {$code} {$message}", $trace );
            }

            protected function purge_older_images( $images, $newest_image ) {
                foreach ( $images  as $file_name => $to_remove ) {
                    if ( "{$file_name}" !== "{$newest_image}" ) {
                        if( is_file( "{$to_remove}" ) && is_writeable( "{$to_remove}" ) ) {
                            if ( $this->is_debug ) {
                                $this->notices[] = "Would now be removing {$to_remove}";
                            } else {
                                $removed = unlink( "{$to_remove}" );
                                if ( !$removed ) {
                                    $this->notices[] = "Failed to remove image: {$to_remove}";
                                }
                            }
                        }
                    }
                }
            }

            # Use the _privateer_web_cam_loading filter to get the script to load where wanted
            public function do_setup_javascript() {
                $do_js = apply_filters('_privateer_web_cam_loading', false, $this->image_tag_id);
                if ( $do_js ) {
                    add_action('get_header', array($this, 'do_register_js') );
                    add_action('wp_head', array($this, 'do_enqueue_js'));
                }
            }
            public function do_register_js() {
                wp_register_script('privateer_web_cam', plugins_url( '/js/web_cam_checker.js', __FILE__ ), array( 'jquery', 'heartbeat' ), "0.0.2", true);
            }

            public function do_enqueue_js() {

                $web_cam_config = array(
                    'image_id' => "{$this->image_tag_id}",
                    'refresh_interval' => (int)$this->refresh_interval_s,
                    'debug' => (int) $this->is_debug,
                    'init_retry_ms' => $this->init_retry_ms,
                    'min_init_retries' => $this->min_init_retries
                );
                wp_localize_script('privateer_web_cam', 'pri_web_cam_settings', $web_cam_config );

                wp_enqueue_script('privateer_web_cam');
            }

            function __destruct() {
                do_action('_privateer_web_cam_runtime_errors', $this->errors);
                do_action('_privateer_web_cam_runtime_notices', $this->notices);
            }
        }

        function do_choose_privateer_web_cam_where_to_load($load, $image_id) {
            if ( 'main_cam_image' == "{$image_id}" && is_front_page() ) {
                $load = true;
            }
            return $load;
        }
        add_filter( '_privateer_web_cam_loading', '\\Privateer\\WebCam\\do_choose_privateer_web_cam_where_to_load', 10, 2);

        # Create up an object to handle the web cam and provide wanted defaults
        # Do this multiple times if you will be using different cam directories and/or image tags
        $o_privateer_web_cam = new Privateer_Do_Web_Cam_Updates(
            'main_cam_image', '', '', 0, 0, 0, true, false
        );
        if ( is_a( $o_privateer_web_cam, '\Privateer\WebCam\Privateer_Do_Web_Cam_Updates' ) ) {
            # Set up the ajax responses
            add_filter( 'heartbeat_received', array($o_privateer_web_cam, 'do_process_heartbeat_received'), 10, 3 );
            add_filter( 'heartbeat_nopriv_received', array($o_privateer_web_cam, 'do_process_heartbeat_received'), 10, 3 );

            # Set up the javascript for the front end on templates that you want it used on
            if ( !is_admin() ) {
                add_action( 'get_header', array($o_privateer_web_cam, 'do_setup_javascript'), 9 );
            }
        } else {
            throw new \Exception('Failed to create Privateer_Do_Web_Cam_Updates object', 10001);
        }
    }
} catch ( \Exception $e ) {
    $code = $e->getCode();
    $message = $e->getMessage();
    $trace = $e->getTraceAsString();
    $line = $e->getLine();
    $file = $e->getFile();
    $err = new \WP_Error( "Error: {$file}(line {$line}): {$code} {$message}", $trace );
    do_action('_privateer_web_cam_init_errors', $err);

    if ( WP_DEBUG ) {
        wp_die( "Error in {$file} on line {$line}: Code:{$code}, Message: {$message}, Trace: {$trace}" );
    }
}

Le constructeur Paramètres:

  • image_tag_id: la valeur dans l'attribut id de l'image à afficher via
  • images_dir: chemin complet du répertoire sur le serveur contenant les images pour cette caméra
  • images_url: les images accessibles dans le monde entier peuvent être consultées à l'adresse
  • refresh_interval_s: secondes entre les pulsations de cette caméra
  • init_retry_ms: pendant init, combien de millisecondes à attendre entre les tentatives
  • min_init_retries: le minimum de tentatives d’initialisation avant d’abandonner
  • is_debug: afficher ou non les messages de débogage sur la console js
  • do_purge_images: supprime ou non les anciennes images dans le répertoire images_dir

Remarque: images_dir et images_url supposent que vous avez créé une cam-images dans le répertoire racine du plug-in. Vous pouvez cependant les définir comme vous le souhaitez.

Choisir les pages qui ont besoin de la webcam javascript

J'ai choisi par défaut de ne pas charger le javascript et d'utiliser un filtre pour permettre à l'utilisateur de choisir les pages qui ont besoin du script.

function do_choose_privateer_web_cam_where_to_load($load, $image_id) {
    if ( 'main_cam_image' == "{$image_id}" && is_front_page() ) {
        $load = true;
    } else if ( 'second_cam_image' == "{$image_id}" && is_page('cam_two') ) {
        $load = true;
    }
    return $load;
}
add_filter( '_privateer_web_cam_loading', '\\Privateer\\WebCam\\do_choose_privateer_web_cam_where_to_load', 10, 2);

Notez que j'ai changé celui-ci pour supposer que deux objets cam sont en cours de chargement:

  1. Une avec id = "main_cam_image" sur la page d'accueil
  2. Une autre ayant id = "second_cam_image" sur une page avec le slug 'cam_two'

Ajustez comme vous le souhaitez avec les différentes fonctions is_ * de WordPress pour que le fichier javscript soit chargé à l’emplacement souhaité.

En dehors de cela, la configuration principale le suit immédiatement.

Si vous souhaitez créer un objet cam ailleurs (par exemple, un fichier functions.php), vous devez être sûr d'utiliser l'espace de noms (comme dans l'exemple suivant):

$o_cam_two = new \Privateer\WebCam\Privateer_Do_Web_Cam_Updates(
    'cam_two', '', '', 0, 0, 0, true, false
);
if ( is_a( $o_cam_two, '\Privateer\WebCam\Privateer_Do_Web_Cam_Updates' ) ) {
    # Set up the ajax responses
    add_filter( 'heartbeat_received', array($o_cam_two, 'do_process_heartbeat_received'), 10, 3 );
    add_filter( 'heartbeat_nopriv_received', array($o_cam_two, 'do_process_heartbeat_received'), 10, 3 );

    # Set up the javascript for the front end on templates that you want it used on
    if ( !is_admin() ) {
        add_action( 'get_header', array($o_cam_two, 'do_setup_javascript'), 9 );
    }
}

Section deux: Le fichier Javascript Tout conseil serait grandement apprécié par ceux qui sont plus familiers avec javascript. Je suis en train de l'apprendre, mais j'ai fait de mon mieux. Cela fonctionne ... et c'est quelque chose.

jQuery(document).ready(function($) {

    (function( document, config ) {

        var settings = {
            $cam_image: null,
            cam_data: {
                current_image_src: null
            },
            image_id: config.image_id,
            debug: parseInt( config.debug ),
            document: document,
            tick_interval: parseInt( config.refresh_interval ),
            waited: 0,
            max_wait: parseInt( config.init_retry_ms ),
            wait_delay_s: parseInt( config.min_init_retries )
        };

        function do_trigger(type, caller, problem ) {
            console.log('Triggering ' + type + ', Caller: ' + caller + ', Problem: ' + problem);
            if ( 'warning' === type ) {
                settings.$document.trigger('web-cam-warning', caller + ': ' + problem);
            } else {
                settings.$document.trigger('web-cam-error', caller + ': ' + problem);
            }
        }

        function do_enqueue_image(data) {
            console.log('Trying to enqueue image...');
            if ( ! wp.heartbeat.enqueue('web_cam_checker_' + settings.image_id, data, true ) ) {
                do_trigger('error', 'do_enqueue_image', 'Failed to add to wp.heartbeat.enqueue. Data: ' + JSON.stringify( data ));
            } else if ( settings.debug ) {
                console.log( 'Queued: ' + JSON.stringify( wp.heartbeat.getQueuedItem('web_cam_checker_' + settings.image_id) ) );
            }
        }

        function do_process_response(el, data) {
            if ( settings.debug ) {
                console.log( 'process_response:' );
                console.log( '######\n ' + 'el: ' + JSON.stringify(el) + '\n######' );
                console.log( '######\n ' + 'data: ' + JSON.stringify(el) + '\n######' );
            }
            if ( data['webcam_new_uri'] ) {
                if ( settings.debug ) {
                    console.log('Found webcam_new_uri: ' + data['webcam_new_uri']);
                }
                settings.cam_data.current_image_src = data['webcam_new_uri'] + '';
                settings.$cam_image.prop('src', settings.cam_data.current_image_src);
                var worked = do_swap_current_image();
                if ( worked ) {
                    if ( settings.debug ) {
                        console.log( 'Swam image worked, setting up next heartbeat queue.' );
                    }
                    do_enqueue_image(settings.cam_data);
                }
            } else {
                if ( data['notice'] ) {
                    if ( settings.debug ) {
                        console.log('Notice Received: ' + data['notice'] + '\nSetting up next heartbeat queue.');
                    }
                    do_enqueue_image(settings.cam_data);
                } else if ( data['error'] ) {
                    do_trigger('error', 'do_process_response', data['error']);
                }
                if ( settings.debug ) {
                    console.log('Full Data: ' + JSON.stringify(data) );
                }
            }
        }

        function do_swap_current_image() {
            var worked = false;

            if ( settings.debug ) {
                console.log('attempting image swap');
            }
            var updated_src = settings.cam_data.current_image_src;
            $("<img/>")
                .one('load', function() {
                    if ( settings.debug ) {
                        console.log('Finished updating to ' + $(this).prop('src'));
                    }
                    worked = true;
                })
                .prop('src', updated_src )
                .each(function(){
                    if ( this.complete ) {
                        $(this).trigger('load');
                    } else {
                        //do_trigger('error', 'do_swap_current_image', 'Did not finish updating to ' + $(this).prop('src'));
                        worked = true
                    }
                });

            return worked;
        }

        function do_setup_timeout( waiting_on ) {
            settings.waited += 1;
            if ( settings.waited < settings.max_wait ) {
                setTimeout( do_init(), settings.wait_delay_s * 1000 );
            } else {
                do_trigger('error', 'do_setup_timeout', 'Giving up on ' + waiting_on + ' (waited ' + settings.waited + ' times)');
            }
        }

        function do_init() {
            if ( typeof window.wp === 'undefined' ) {
                do_setup_timeout('window.wp');
            } else if ( typeof window.wp.heartbeat === 'undefined' ) {
                do_setup_timeout('window.wp.heartbeat');
            } else if ( typeof settings.image_id === 'undefined' ) {
                do_trigger('error', 'do_init', 'Cannot start web cam without html image tag id name');
            } else {
                settings.$cam_image = $('#' + settings.image_id);
                console.log('Settings:' + JSON.stringify(settings.$cam_image));
                if ( 0 === settings.$cam_image.length ) {
                    do_trigger('error', 'do_init', 'Failed to locate image #' + settings.image_id);
                } else {
                    if ( settings.interval < 5 ) {
                        do_trigger('warning', 'do_init', 'Interval cannot be shorter than 5 seconds. Detected as ' + settings.interval );
                        settings.interval = 5;
                    } else if ( settings.interval > 120 ) {
                        do_trigger('warning', 'do_init', 'Interval cannot be longer that 120 seconds. Detected as ' + settings.interval );
                        settings.interval = 120;
                    }
                    settings.cam_data.current_image_src = settings.$cam_image.prop('src');
                    console.log('Settings Now: ' + JSON.stringify( settings ));
                    do_enqueue_image( settings.cam_data );
                    document.on('heartbeat-send', function(el, data) {
                        if ( settings.debug ) {
                            console.log('Data sent was ' + JSON.stringify( data ));
                        }
                    }).on('heartbeat-tick.web_cam_checker_' + settings.image_id, function(el, data) {
                        console.log('detected heartbeat tick:' + JSON.stringify(el));
                        if ( data.hasOwnProperty('web_cam_checker_' + settings.image_id) ) {
                            if ( settings.debug ) {
                                console.log('Data has web_cam_checker_' + settings.image_id);
                            }
                            do_process_response(el, data['web_cam_checker_' + settings.image_id]);
                        } else if ( settings.debug ) {
                            console.log('Data lacks web_cam_checker_' + settings.image_id + ': ' + JSON.stringify(data) );
                        }
                    });
                    wp.heartbeat.interval(settings.tick_interval);
                }
            }
        }

        do_init();

    })( $(document), pri_web_cam_settings );

    $(document)
        .on('web-cam-error', function(e) {
            console.log('Web Cam Error: ' + e);
        })
        .on('web-cam-warning', function(e) {
            console.log('Web Cam Warning: ' + e);
        })
        .on('heartbeat.error', function(e) {
            console.log('Heartbeat Error: ' + JSON.stringify(e) );
        });

});

Merci pour l'idée Kaiser. Je n'avais pas entendu parler de l'API Heartbeat et je cherchais quelque chose à essayer en développant les connaissances en javascript sur ... c'était donc un bon exercice.

Je ne l'ai essayé que sur un serveur LAMP en cours de navigation depuis Firefox ... et non, je n'ai pas encore fait de déclaration stricte ... mais je pourrais le faire la prochaine fois.

Quoi qu'il en soit, j'espère que cela aidera un peu quelqu'un.

Tout pour ceux qui débutent dans le code ...

Pour que cela fonctionne tel quel:

  1. Créez un répertoire dans votre répertoire wp-content/plugins (nommez-le comme bon, j'utiliserai privateer-web-cam-updates)
  2. Créez un nouveau fichier texte nommé privateer-web-cam-updates.php dans ce répertoire et ouvrez-le dans un éditeur de texte brut.
  3. Ajoutez un id = "main_cam_image" à une balise d'image sur votre site (par exemple, votre page d'accueil). Cela pourrait ressembler à <img src="#" id="main_cam_image" title="My Web Cam" />
  4. Éditez la section de code suivante dans le plugin.

function do_choose_privateer_web_cam_where_to_load($load, $image_id) { if ( 'main_cam_image' == "{$image_id}" && is_front_page() ) { $load = true; } return $load; } add_filter( '_privateer_web_cam_loading', '\\Privateer\\WebCam\\do_choose_privateer_web_cam_where_to_load', 10, 2);

Si vous avez votre image sur un blog, remplacez is_front_page() par is_home(). Si vous l'avez sur une page, obtenez l'ID de la page, puis remplacez is_front_page() par is_page(n) où n est l'ID de la page.

  1. Créez un sous-répertoire cam-images dans le dossier du plug-in et configurez votre webcam pour y placer les images.
  2. Créez un sous-répertoire js dans le dossier du plugin.
  3. Copiez le code de la section 2 dans un nouveau fichier texte nommé web_cam_checker.js

Et, encore une fois, les critiques constructives sont toujours appréciées. (impossible d'obtenir la dernière section détectée comme code pour une raison quelconque)

1
Privateer