Friday, 3 April 2026

Finding Duplicate Photos in Amazon Photos with a Terminal Command

If you use Amazon Photos to back up your pictures, you might have noticed something frustrating over time: duplicate photos. It happens to everyone — you upload a batch of photos twice, you edit a picture and keep both versions, or your phone syncs a photo you already had. Before you know it, your library is cluttered with copies you don't even remember making.                                    

I recently built a command-line tool that connects to your Amazon Photos library and automatically finds those duplicates for you. Let me walk you through how it works.                                  

                                                                                                                                                 The problem with finding duplicates the old way                                                                                                                                                          

The naive approach to finding duplicate files is to compare them byte by byte — if two files are identical down to the last bit, they're duplicates. But photos don't work that well like that. The same picture can exist in your library in slightly different forms: different compression, a small crop, a brightness tweak, or even a different file format (JPEG vs. PNG). Byte-by-byte comparison would miss all of those.                                                                                                                                                                                            

How the tool actually detects duplicates

The tool uses a technique called perceptual hashing. Instead of comparing raw file data, it looks at what the image looks like to the human eye. It converts each photo into a compact "fingerprint" that  captures its visual content, then measures how similar two fingerprints are — giving a percentage from 0% (completely different) to 100% (visually identical).

This means it can detect duplicates even when the files are technically different, as long as the pictures look the same. 

                                                                             

A two-phase process to avoid downloading everything                                                                                                                                                       

Downloading and visually comparing thousands of photos one by one would take forever. The tool is smarter than that: it first does a quick metadata pass to narrow down candidates.

Photos with the same filename, or taken at the exact same second, are very likely to be duplicates. The tool groups those photos together first — without downloading any images. Only then does it download and visually compare the photos within each group. This makes the whole process much faster.


Running the command                                                                                                                                                                                       

The command is photos:find-duplicates. In its simplest form, you just run it and let it scan your entire library:                                                                                         

php artisan photos:find-duplicates                                                                                                                                                                        

You can also narrow things down. For example, to only look at photos uploaded in the last 30 days:                                                                                                        

 php artisan photos:find-duplicates --uploaded-last-days=30                                                                                                                                                

 Or to compare photos taken within a specific date range:

 php artisan photos:find-duplicates --taken-between=01/01/2024,31/12/2024                                                                                                                                  

By default, it groups candidate duplicates by filename. You can also group by the timestamp when the photo was taken, or both at once:                                                                    

php artisan photos:find-duplicates --group-by=taken-at                                                                              php artisan photos:find-duplicates --group-by=name-and-taken-at


The similarity threshold defaults to 90% — photos that look at least 90% alike are flagged as duplicates. You can make it stricter or more lenient:                                                       

php artisan photos:find-duplicates --similarity=95                                                                                                                                                        

Comparing two specific photos                                                                                                                                                                             

Sometimes you already suspect two specific photos are duplicates. Instead of scanning the whole library, you can compare just those two by passing their IDs directly:                                    

php artisan photos:find-duplicates photo-id-1 photo-id-2                                                                                                                                                  

 The tool will tell you the similarity percentage and whether they're considered duplicates. 


More info: https://github.com/icsbcn/awsphotosapi                                                                                                         

Wednesday, 1 April 2026

Never Lose a Photo Again: How I Built a Tool to Organize My Amazon Photos

If you use Amazon Photos to store your pictures, you probably know how easy it is to end up with hundreds — or even thousands — of photos just sitting there, not organized in any album. That was exactly my problem.

I love taking photos. But organizing them? Not so much. Over the years I accumulated a huge collection on Amazon Photos, and most of it was just a big unsorted pile. Albums existed, but plenty of photos never made it into one.

So I built a small tool to help me fix that.

Sunday, 9 March 2025

Nodes 2: Renovation of the websites of the Àgora educational centers

 https://ithinkupc.com/en/news/nodes-2-renovation-of-the-websites-of-the-agora-educational-centers/

Thursday, 6 March 2025

Xampp: Cannot create file "C:\xampp-control.ini"

It may happen that sometimes when installing and/or using xampp on Windows without administrative permissions this message appears.

In this case, my recommendation is to gain administrative permissions for a while and do:

  1. In the directory where xampp is installed, give "full control" permissions to all users for these 2 files:
    1. xampp-control.exe
    2. xampp-control.ini
Regards!

Tuesday, 25 July 2017

Moodle: Force users to viewn default "My Moodle"

Hello,

The best option to control content of "my" section and prevent users from changing settings is:

- Add in config.php this line:

$CFG->forcedefaultmymoodle = true;

Best regards,
Iban Cardona.

Thursday, 13 July 2017

Moodle: Change course name in breadcrumb

Hello,

If you want to modify the text of the a course in the breadcrumb you can do this (tested in Moodle 3.2):

Add this function in your file lib.php of your local plugin:

function local_yourlocalplugin_extend_navigation(global_navigation $nav) {
    global $COURSE, $CFG;

    if (isset($COURSE->id) && $COURSE->id > 0) {
        $coursenode = $nav->find($COURSE->id, navigation_node::TYPE_COURSE);
        if ($coursenode) {
                $coursenode->text .= ' My extra text';
        }
    }
}

Best regards!

Monday, 15 May 2017

Moodle & PHPUnit: Capture events

Hello,

If you want to tests events in Moodle using PHPUnit, the best method is:

public function test_creationg_event() {
        $this->resetAfterTest();
        $event_sink = $this->redirectEvents();

        $course = $this->getDataGenerator()->create_course();

        $events = $event_sink->get_events();
        $event_sink->close();
        $event = $events[0];
        $this->assertInstanceOf('\core\event\course_created', $event);
}

Best regards!

Tuesday, 25 April 2017

Moodle: Block settings not shown for students

Hello,

If you want to disable (not shown) settings block for students in Moodle, you can do it changing the renderer in your theme. This code is valid only for themes not based on Boost.

Add this function in the core_renderer class of your theme:

public function block(block_contents $bc, $region) {
        global $DB;

        $idblock = isset($bc->attributes['id']) ? $bc->attributes['id'] : '';
        $arr_aux = explode('inst', $idblock);
        if (isset($arr_aux[1])) {
                $block_instance = $DB->get_record('block_instances', array('id' => $arr_aux[1]));
                $context_course = context_course::instance($this->page->course->id);
                if (isset($block_instance->blockname) && $block_instance->blockname == 'settings') {
                    if (!has_capability('moodle/grade:viewall', $context_course)) {
                        return '';
                    }
                }
        }

        return parent::block($bc, $region);
}

Best regards,
Iban Cardona.

Wednesday, 22 February 2017

Moodle: moodle_form inside module: Edit or new?

Hello,

Sometimes inside a custom Moodle module we need to know if we are editing or creating a new module (activity or resource) instance... Here the solution:

function definition() {
        global $CFG, $DB;
        $mform =& $this->_form;

        if (is_numeric($this->_instance) && $this->_instance && $mymodule = $DB->get_record("mymodule", array("id"=>$this->_instance))) {
            $mform->addElement('header', 'general', 'Editing');
        } else {
            $mform->addElement('header', 'general', 'Creating');
        }

...

Best regards,
Iban Cardona.

Saturday, 7 January 2017

Moodle: Download exact commit from git

Hello,

If you want to manage a source code from Moodle of a specific commit, you need to do:

  • Install git: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git
  • Find the commit to download in https://github.com/moodle/moodle . In my case I found this: https://github.com/moodle/moodle/commit/89117976b8afd75c588b8b3908f763aab2edfb6a#diff-5bbd3ccd25214f9956eea7e9f714bc08
  • Download the Moodle code: git clone https://github.com/moodle/moodle.git my_moodle
  • Copy the ID of the commit: 89117976b8afd75c588b8b3908f763aab2edfb6a
  • Checkout the commit:
    • cd my_moodle
    • git checkout 89117976b8afd75c588b8b3908f763aab2edfb6a

Best regards! 

Monday, 22 August 2016

Laravel : Get SQL of a DB object

Hello,

How to print SQL (Select statement) of DB Object in Laravel:

$sql = DB::table('users');
echo $sql->toSql();

Best regards,
Iban Cardona.

Thursday, 28 July 2016

Laravel 4.2 Add get params in URL::action helper function

Hello,

If we want to add get params we need to call function like this:

routes.php

Route::any('test/{id}',  'Controller@index');

blade.php

{{URL::action("Controller@index", array(111, "firstparam" => "aaa"))}}

And result is:
http://www.dummyurl.local/test/111?firstparam=aaa

Best regards,
Iban Cardona.

Tuesday, 10 May 2016

PHP: String HH:MM:SS to hours (float)

Hello,

How to parse string with format HH:MM:SS to hours:

$string = '01:58:03';
sscanf($string, "%d:%d:%d",  $hours, $minutes, $seconds);
$hours = $hours + $minutes/60 + $seconds/3600;

Best regards!
 

Thursday, 28 April 2016

Laravel 5.2: Create multilang site (Session+Middleware)

Hello,

How to create a multi-language site in Laravel 5.2:

config/app.php

'locale' => 'ca',
'locales' => ['ca' => 'Català', 'es' => 'Español', 'en' => 'English'],

app/Http/routes.php

Route::group(['middleware' => ['web']], function () {
    Route::get('/', [
        'as' => 'underconstruction', 'uses' => 'Underconstruction\UnderconstructionController@showIndex'
    ]);
    Route::get('/lang/{langcode?}', function ($langcode = 'ca') {
        return redirect()->route('underconstruction');
    });
});


app/Http/Kernel.php

/**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \App\Http\Middleware\LangMiddleware::class,
        ],

        'api' => [
            'throttle:60,1',
        ],
    ];


/app/Http/Middleware/LangMiddleware.php

<?php

namespace App\Http\Middleware;

use Closure;
use App;

class LangMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $lang = $request->segment(1);
        $locale = $request->segment(2);

        if ($lang == 'lang' && array_key_exists($locale, app()->config->get('app.locales'))) {
            $request->session()->put('locale', $locale);
        } else if ($request->session()->has('locale')) {
            App::setLocale($request->session()->get('locale'));
        }

        return $next($request);
    }
}


/app/Http/Controllers/Underconstruction/UnderconstructionController.php

<?php

namespace App\Http\Controllers\Underconstruction;

use App\Http\Controllers\Controller as Controller;

class UnderconstructionController extends Controller
{
    public function showIndex() {
        return app()->getLocale();
    }
}


If you access to http://yourweb/ --> lang = ca
if you access to http://yourweb/lang/es --> lang = es
If you access to http://yourweb/ --> lang =es

Lang is saved in session data.

Best regards,
Iban Cardona.

Thursday, 31 March 2016

Init class of a unnamed namespace inside other namespace

Hello,

How to init a class without namespace in another class with namespace:

<?php

class first_class {
   public function  __construct() {
   }
}

And:

<?php

namespace namespace1\namespace2;

class second_class {
         public function  __construct() {
         }

         public function test() {
                $object = first_class(); --> ERROR!!!!
                $object = \first_class(); --> OK
         }
}

Best regards!

Wednesday, 30 March 2016

Moodle: Get courses recursively

Hello,

How to get all courses of a category recursively?

<?php

global $CFG;
require_once($CFG->dirroot . '/lib/coursecatlib.php');
$categoryid = 1;
$allcourses = coursecat::get($categoryid)->get_courses(array('recursive' => true));

How to get all courses of a hidden category?

<?php

global $CFG;
require_once($CFG->dirroot . '/lib/coursecatlib.php');
$categoryid = 1;
$allcourses = coursecat::get($categoryid, MUST_EXIST, true)->get_courses(array('recursive' => true));

Best regards!

Thursday, 3 March 2016

PHP : How to access to an object variable whose name is another variable of the object

Hello,

If we suppose that:

<?php
class test_class
{
       private $variable_name = 'id';
       private $id  = 0;
}

How to acces to variable 'id' inside an instance? :

<?php

class test_class
{
       private $variable_name = 'id';
       private $id  = 0;

       function test() {
             echo $this->$this->variable_name;  // ERROR!!!!
             echo $this->{$this->variable_name};
       }
}

$object = new test_class();

$object->test();

Best regards!

Wednesday, 10 February 2016

Moodle: Create zip file with all course resources

Hello,

How to create a zip file with all resource modules of a course:

<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

require_once('../../config.php');

$id = required_param('id', PARAM_INT);

$course = get_course($id);
if (!$course) {
    die();
}

$context = context_course::instance($course->id);

require_login($course);

$filename = 'contents.zip';
$new_names = array();

// Get files resources
$query_resources = "SELECT {resource}.*
FROM {resource}, {course_modules}, {modules}
WHERE {course_modules}.instance = {resource}.id
AND {modules}.id = {course_modules}.module
AND {modules}.name = 'resource'
AND {resource}.course = ?
AND {course_modules}.course = ?";
$resources = $DB->get_records_sql($query_resources, array($course->id, $course->id));

foreach ($resources as $resource) {
    $cm = get_coursemodule_from_instance('resource', $resource->id, $resource->course, false, MUST_EXIST);
    $context_resource = context_module::instance($cm->id);
    $fs = get_file_storage();
    
    $resource_files = $fs->get_area_files($context_resource->id, 'mod_resource', 'content', 0, 'sortorder DESC, id ASC', false);
    if (count($resource_files) < 1) continue;
    
    $resource_file = reset($resource_files);
    unset($resource_files);
    
    $contenthash = $resource_file->get_contenthash();
    $l1 = $contenthash[0].$contenthash[1];
    $l2 = $contenthash[2].$contenthash[3];
    $filedir = ((isset($CFG->filedir))) ? $CFG->filedir : $CFG->dataroot.'/filedir';
    $path = "$filedir/$l1/$l2/$contenthash";
    
    $new_names['/'.$resource->name.'/'.$resource_file->get_filename()] = $path;
}
// End get files resources

// Create zip file
$packer = get_file_packer('application/zip');
$fs = get_file_storage();
$result = $packer->archive_to_storage($new_names, $context->id, 'local_zip', 'zip_files', 0, '/', $filename);
// End Create zip file

print_r($result);

Best regards,
Iban Cardona.

Tuesday, 2 February 2016

Moodle: Use config.php vars (CFG) loading minimum data

Hello,

Sometimes, in a custom moodle script or in your local plugin you need $CFG vars but you don't need all moodle features.
The best option is write:

<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

define('NO_DEBUG_DISPLAY', true);
define('ABORT_AFTER_CONFIG', true);
require_once('../../config.php');

...

With this code, you could, for example, execute echo $CFG->dbhost

Best regards,
Iban Cardona.

Monday, 4 January 2016

How we recovered my iPhone from water

Hi!

On December 25 my iPhone 5 had an accident: it fell in the washing machine along with my dirty clothes. This is the story of what happened and how we recovered it.

Late on the 25th we get home from the family Christmas lunch and it´s time to give our son (7 months) his snack, when disaster strikes: I knocked over all the baby food! Soaked jeans and leather shoes all wet...

While the child was crying disconsolately, I took off my clothes and left them on the floor. Then, half naked, I went to prepare a new snack for him while my wife put the dirty clothes in the washing machine.

Finally, the baby got to eat and everyone was happy.

20 minutes later, I asked:
- Darling, have you seen my phone?
- No.
- Are you sure? Strange, as I always have it the pocket of my... F%%K F%%K F%%K !!!

After some well due cursing, we started our attempts to recover the phone:

 1 - Stop the washing machine. Although it may seem obvious, I can assure you that the first reaction is to watch as the drum rotates and rotates round with the phone inside it.
2 - If the phone is on, turn it off.
3 - Wipe the entire phone with a dry cloth. All small holes and slots too.
4 - Immerse the phone for 36 hours in white rice. If you notice that the rice gets very wet, change it.
5 - Try turning on the phone. If after that, the screen still has many wet patches, shut the phone off again and put it back into the rice.

After nearly two days the iPhone will come back to life and hopefully it will be 90% functional (Some areas of the screen will not be recovered).