Thursday, December 29, 2016

A new NAS

After 10 years (or so) of using a PC as a file server, I finally decided enough is enough.  The size, noise and upkeep of my old PC was just a bit too much for me to want to deal with any longer.  Not that there’s an issue with using a PC as a file server, but it just seems, now, a bit overkill.

My original goal when I build the PC server was to use virtualization in order to run various servers and experiment with various technologies.  While that worked for a time, the amount of memory I needed ended up growing beyond what I have and the price to upgrade became too much for what is basically an eight-year-old machine.

The size also became a bit too much to handle.  When I built the machine originally, I bought a huge full tower case.  I assumed that I would ultimately need multiple HDDs and a big power supply.  Had I stuck with the virtualization, then this may have made more sense.  Now, it’s just a noisy boat anchor that serves (pun intended) as a file and iTunes server.

One other reason to move away starts with the advent of streaming music services (Google Play, which I use).  I now rely much less on the iTunes sharing than ever before.  My kids have a bedtime play list they use every night to go to sleep, but other than that (and the occasional Sonos use), I don’t rely on it as heavily as before.

With this in mind, I started looking at stand alone Network Attached Storage devices.  I like the idea of a bespoke device to manage this setup.  I also like the size of the device as opposed to beastie I have now.  The new devices also appear to offer more functionality and can, basically, replace a lot of the things I needed a stand alone PC for before.

If you’re curious, I ended up with a Synolog DS416play.  It was slightly more expensive than I was planning on, but it does offer better media capabilities, including hardware-based media transcoding.  This was something I’d tried on the old system and it just couldn’t keep up.  I’m hoping to dump much of my media on there and share them out to my Chromecast and Roku devices.

As I spend more time with it, I’ll post more.

Smart House

Earlier this year, I finally broke down and purchased a Samsung SmartThings home automation hub.  I had wanted to get one for about two years after hearing about them advertised on the TWiT network. I did wait a bit until I had a bit of spare cash and for the SmartThings 2.0 hub to come out.  The big draw for the 2.0 was some limited local network-only remote control.

Anyway, it started with just a Z-wave front door lock and has begun to spread.  I now have several receptacle and smart appliance switches, garage door opener, door sensor and a number of Aeotec water sensors.

In fact, those just saved me today when we had a small water problem in the basement.  Somehow the hot water was left on and that caused a poorly installed trap to slip just enough to let water start leaking onto the floor.

When I bought the sensors originally, that was the very first place I intended to put one because this has happened to me before.  The sink is a pedestal sink which makes it hard to get access to the trap in case of a leak.  Well, when it happened before it took me a while to catch it and I’ve been a bit paranoid about it happening again since.

Well, this time the sensor saved me.  The app first indicated a water problem in the basement at 1:34pm.  By 1:45ish, I’d fixed the problem and cleaned up the mess.

Oh and thanks for the assist goes to Pushbullet.  I’ve been using it for a number of years to handle notification mirroring from my Android phones to Chrome on my PCs.  In this case, the notification popped up on my laptop while I was doing something else, so I was able to immediately react.  For lack of that, I may not have caught it until a bit later.  Come to think of it, I may want to update the action on the water problems to include an email …

Tuesday, December 27, 2016

More Details on Photo Sorting

After about three weeks of work, I believe I’ve identified almost 167,000 unique pictures (give or take a thousand or so) and over 250k duplicates.  Right now, OneDrive is struggling to determine what of the myriad of changes I’ve made are valid and trying to sync that up to the cloud.  I sort of expect that to take quite some time.

I still haven’t resorted them based on the Year/Month/Day/Model mode I mentioned earlier, but I do plan on doing that eventually.  First I need to let this sync happen and then I plan on identifying which cameras belong to us, family and friends so I can separate ours/theirs/etc.  Once that’s done, then I’m going to slowly perform the final reorgs of Mobile/Regular and apply the new sort.

I’m also considering whether I should separate out all the videos or not.  Part of me says yes, part of me says no.  Having the videos separate might make it easier to prepare videos and other multimedia things in the future, but there’s also something nice about having the pictures/videos intermingled and can lead to some neat discoveries.

I’ll post more as I go.

Sunday, December 18, 2016

Sorting out Photos Revisited

Since my last post, I’ve continued running the script to process my images and am getting close to being done.  Given that, there are a few things I would like to have done differently:

  • Right now the script organizes pictures as: <model>/<year>/<month>/<day>/<picture/video>  While that works ok, it does mean that to find a specific picture I need to know which camera model took it. I’m starting to think I should have turned that around: <year>/<month>/<day>/<model>.  At least then I would only need to know the approximate day they were taken.  However, I do like the idea of grabbing all mobile phone pics/video and being able to move them en masse.
  • Videos don’t, by default, include the camera model.  Not really sure why this is, but that does make it kind of annoying to sort out.   I have to build some strange rules/heuristics/guesses to determine which camera took what picture.  Maybe not an issue for deduping, but definitely a pain if I want to keep them separated into Mobile/Non-Mobile.
  • MacOS’s mdls command does a great job at dragging the capture date from videos, but only if the file is mounted locally on the Mac itself.  This includes FAT32 and other Windows-based file systems.  If you mount the same file system via NTFS, it gets confused.  As such, I’ve had to leave behind AVI files so I can pull them local later.  (Though, as I think of it, maybe mediainfo would work for AVI files … not sure why I didn’t try that)
  • My previous script only looks at the first conflict, meaning that once I have a single non-duplicated file, any more duplicates may be copied repeatedly.  That’s my next correction.

Overall, the process has been a bit of a pain in the back side and definitely slow, but I’m almost to the point where I have one canonical copy of all my pictures.  (Well, actually two because I’m working off my local OneDrive mirror).

Sunday, December 11, 2016

Sorting Out Photos

My wife and I are (or at least were) shutter bugs of a sort.  At this moment, I have just a bit shy of 1.5 terabytes of photos that she and I have take over the years.  I’ve also managed to make a hash of them with copies, duplicates and the occasional “I think I have this somewhere, but I can’t say for certain” directory.

I’ve been looking for the past year or two for a solution and still haven’t really found one I liked, so like a good nerd, I’ve rolled my own.  It’s cobbled together using BASH, ImageMagick, dcraw and MediaInfo.

My primary goal was to make sure that I had one copy of every file, not necessarily one high quality version of each picture or video.  Meaning, that if I end up with duplicated of a picture in RAW, high res JPEG and a JPEG thumb, I’m ok with that.  Once I have the initial culling of the photos, then I make take another swipe at further deduping it.

Anyway, my script starts by recursively looping through the current path and all subdirectories.  If it encounters a file, it will retrieve the extension and then conditionally call some combination of the above utilities to retrieve the creation/capture/modified timestamp and the camera make/model.  It does this fairly well, but there are some major caveats which I’ll discuss in a bit.

Once it has retrieved the above, it starts creating the following folder structure:

/<camera>/<year>/<month>/<day>

It then takes the file and tries to copy it into the following:

/<<camera>/<year>/<month>/<day>/<year><month><day><hour><minute><second>.<#>.<ext>

This should, in theory, allow me to identify a specific picture taken by a specific type of camera at a specific moment in time.  The initial issue I ran into with this is around time resolution.  The timestamps given to me by the various tools only resolve to the second (not millisecond like I’d prefer).  This means that if you have a camera that can take multiple pictures per second, then you can easily end up with duplicates, hence the <#> at the end.

If I encounter a file that is the same timestamp, I then do a MD5 sum on both files to confirm they are actually the same time.  If they are, then off to a duplicates tree the file goes.  If they aren’t the same, then I start an auto increment pass until I can write the file out uniquely in the target folder.

One issue, though, is that if the 0 file doesn’t match, I don’t check for subsequent matches, so the script could easily end up with files 1 2 3 and 4 all being duplicates.  Maybe I’ll try and fix that in a future edit.

As for the tooling, I use the following:

  • BASH
  • ImageMagick’s “identify --verbose”  command to get information a JPEGs
  • MediaInfo for details on MP4/M4V/AVI/MOV files
  • dcraw for details on RAW files (such as Nikon’s NEF/NRW)

Probably one of the biggest issues I have is that while your typical JPG/NRW/NEF file includes the camera details, a video typically does not. That means that I’m a bit hard pressed to determine what camera took a specific video.  I also found that the camera metadata for when the file was captured isn’t always that useful, so there are limits.

One other thing to note: ImageMagick isn’t always that fast, so there’s room for improvement on this, specifically around JPEG processing.  I was hoping to use MacOSs mdls for getting the camera data, but that only works if the filesystem is local (not mounted like mine was).

If you’re a BASH expert, please be kind.  I’m good at programming, not always good at scripting.  Otherwise, help yourself.

#!/bin/bash
BASE="/Volumes/e/Pictures/Processed"

moveFile()
{
#    local SOURCE="$1"
#    local BASE="$2"
#    local EXT="$3"
#    local SUFFIX="$4"
   
    if [ -f "$2.$4.$3" ] ; then
        moveFile "$1" "$2" "$3" $(($4 + 1 ))
    else
        mv -n "$1" "$2.$4.$3"
    fi
}

moveNonDuplicateFile()
{
    mkdir -p "$BASE/Sorted/$2/$3/$4/$5"

    local T="$BASE/Sorted/$2/$3/$4/$5/$3$4$5$6$7$8"

    moveFile "$1" "$T" "$9" "0"
}

#        moveDuplicateFile "$1" "$BASE" "$CAMERA" "$YEAR" "$MONTH" "$DAY" "$HOUR" "$MINUTE" "$SECOND" "$EXT"
moveDuplicateFile()
{
    mkdir -p "$BASE/Duplicates/$2/$3/$4/$5"

    local T="$BASE/Duplicates/$2/$3/$4/$5/$3$4$5$6$7$8"

    moveFile "$1" "$T" "$EXT" 0
}

processMOV()
{
    TIMESTAMP=`mediainfo "$1" | grep "Encoded date" | head -n 1 | sed 's/Encoded date//' | awk '{$1=$1;print}' | sed 's/: //'`
   
    YEAR=`date -ujf "%Z %Y-%m-%d %H:%M:%S" "$TIMESTAMP" +%Y`
    MONTH=`date -ujf "%Z %Y-%m-%d %H:%M:%S" "$TIMESTAMP" +%m`
    DAY=`date -ujf "%Z %Y-%m-%d %H:%M:%S" "$TIMESTAMP" +%d`

    HOUR=`date -ujf "%Z %Y-%m-%d %H:%M:%S" "$TIMESTAMP" +%H`
    MINUTE=`date -ujf "%Z %Y-%m-%d %H:%M:%S" "$TIMESTAMP" +%M`
    SECOND=`date -ujf "%Z %Y-%m-%d %H:%M:%S" "$TIMESTAMP" +%S`
   
    if [ -z "$CAMERA" ] ; then
        CAMERA="MOV"
    fi
}

processRAW()
{
    TIMESTAMP=`dcraw -i -v "$1" | grep Timestamp | sed s/Timestamp\:\ //`
   
    YEAR=`date -jf "%a %b %d %H:%M:%S %Y" "$TIMESTAMP" +%Y`
    MONTH=`date -jf "%a %b %d %H:%M:%S %Y" "$TIMESTAMP" +%m`
    DAY=`date -jf "%a %b %d %H:%M:%S %Y" "$TIMESTAMP" +%d`
   
    HOUR=`date -jf "%a %b %d %H:%M:%S %Y" "$TIMESTAMP" +%H`
    MINUTE=`date -jf "%a %b %d %H:%M:%S %Y" "$TIMESTAMP" +%M`
    SECOND=`date -jf "%a %b %d %H:%M:%S %Y" "$TIMESTAMP" +%S`
   
    CAMERA=`dcraw -i -v "$1" | grep 'Camera:' | awk -F\: '{ print $2 }' | tr '[:lower:]' '[:upper:]' | awk '{$1=$1;print}'`
}

processAVI()
{
    TIMESTAMP=`mdls "$1" | grep kMDItemContentCreationDate | sed 's/kMDItemContentCreationDate     = //'`
   
    if [ "$TIMESTAMP" == "" ] ; then
        YEAR="0000"
        MONTH="00"
        DAY="00"
        HOUR="00"
        MINUTE="00"
        SECOND="00"
    else
        YEAR=`date -jf "%Y:%m:%d %H:%M:%S %z" "$TIMESTAMP" +%Y`
        MONTH=`date -jf "%Y-%m-%d %H:%M:%S %z" "$TIMESTAMP" +%m`
        DAY=`date -jf "%Y-%m-%d %H:%M:%S %z" "$TIMESTAMP" +%d`

        HOUR=`date -jf "%Y-%m-%d %H:%M:%S %z" "$TIMESTAMP" +%H`
        MINUTE=`date -jf "%Y-%m-%d %H:%M:%S %z" "$TIMESTAMP" +%M`
        SECOND=`date -jf "%Y-%m-%d %H:%M:%S %z" "$TIMESTAMP" +%S`
    fi
   
    if [ -z "$CAMERA" ] ; then
        CAMERA="AVI"
    fi
}

processJPG()
{
#    TIMESTAMP=`mdls "$1" | grep kMDItemContentCreationDate | sed 's/kMDItemContentCreationDate     = //'`
#    CAMERA=`mdls "$1" | grep kMDItemAcquisitionModel | sed 's/kMDItemAcquisitionModel        = \"//' | sed s/\"//`

    TIMESTAMP=`identify -verbose "$1" | grep DateTimeDigitized | sed 's/    exif:DateTimeDigitized: //'`
    TIMESTAMP="$TIMESTAMP -0000"
    CAMERA=`identify -verbose "$1" | grep "exif:Model" | sed 's/    exif:Model: //'`

    if [ "$TIMESTAMP" == " -0000" ] ; then
        TIMESTAMP=`identify -verbose "$1" | grep "date:modify" | sed 's/    date:modify: //' | sed 's/\(.*\)-\(.*\)-\(.*\)T\(.*\)\([+-]\)\(.*\):\(.*\)/\1:\2:\3 \4 \5\6\7/'`
        #TIMESTAMP=`mdls "$1" | grep kMDItemFSContentChangeDate | sed 's/kMDItemFSContentChangeDate = //'`
    fi
   
    # | awk '{$1=$1;print}'`
       
    #echo $TIMESTAMP / $IMAGE
   
    # 2014-07-05T11:12:16-04:00
   
    if [ "$TIMESTAMP" == "" ] ; then
        YEAR="0000"
        MONTH="00"
        DAY="00"
        HOUR="00"
        MINUTE="00"
        SECOND="00"
    else
#         YEAR=`date -jf "%Y-%m-%d %H:%M:%S %z" "$TIMESTAMP" +%Y`
#         MONTH=`date -jf "%Y-%m-%d %H:%M:%S %z" "$TIMESTAMP" +%m`
#         DAY=`date -jf "%Y-%m-%d %H:%M:%S %z" "$TIMESTAMP" +%d`
#    
#         HOUR=`date -jf "%Y-%m-%d %H:%M:%S %z" "$TIMESTAMP" +%H`
#         MINUTE=`date -jf "%Y-%m-%d %H:%M:%S %z" "$TIMESTAMP" +%M`
#         SECOND=`date -jf "%Y-%m-%d %H:%M:%S %z" "$TIMESTAMP" +%S`
        YEAR=`date -jf "%Y:%m:%d %H:%M:%S %z" "$TIMESTAMP" +%Y`
        MONTH=`date -jf "%Y:%m:%d %H:%M:%S %z" "$TIMESTAMP" +%m`
        DAY=`date -jf "%Y:%m:%d %H:%M:%S %z" "$TIMESTAMP" +%d`

        HOUR=`date -jf "%Y:%m:%d %H:%M:%S %z" "$TIMESTAMP" +%H`
        MINUTE=`date -jf "%Y:%m:%d %H:%M:%S %z" "$TIMESTAMP" +%M`
        SECOND=`date -jf "%Y:%m:%d %H:%M:%S %z" "$TIMESTAMP" +%S`
    fi
   
   
    if [ -z "$CAMERA" ] ; then
        CAMERA="Unidentified"
    fi
}

processMPEG4()
{
    TIMESTAMP=`mediainfo "$1" | grep "Encoded date" | head -n 1 | sed 's/Encoded date//' | awk '{$1=$1;print}' | sed 's/: //'`
   
    YEAR=`date -ujf "%Z %Y-%m-%d %H:%M:%S" "$TIMESTAMP" +%Y`
    MONTH=`date -ujf "%Z %Y-%m-%d %H:%M:%S" "$TIMESTAMP" +%m`
    DAY=`date -ujf "%Z %Y-%m-%d %H:%M:%S" "$TIMESTAMP" +%d`

    HOUR=`date -ujf "%Z %Y-%m-%d %H:%M:%S" "$TIMESTAMP" +%H`
    MINUTE=`date -ujf "%Z %Y-%m-%d %H:%M:%S" "$TIMESTAMP" +%M`
    SECOND=`date -ujf "%Z %Y-%m-%d %H:%M:%S" "$TIMESTAMP" +%S`

    if [ -z "$CAMERA" ] ; then
        CAMERA="MP4"
    fi
}

processFile()
{
    echo "Processing file $1"
   
    local EXT=`echo "$1" | sed 's/.*\.\([A-Za-z0-9]*\)/\1/' | tr '[:lower:]' '[:upper:]'`

    case $EXT in

        # Picture Formats Here

        NEF)
            processRAW "$1"
            ;;

        NRW)
            processRAW "$1"
            ;;

        JPG)
            processJPG "$1"
            ;;

        # Media Formats Here

        AVI)
            processAVI "$1" "$EXT"
            ;;

        MOV)
            processMOV "$1" "$EXT"
            ;;

        MP4)
            processMPEG4 "$1" "$EXT"
            ;;

        M4V)
            processMPEG4 "$1" "$EXT"
            continue
            ;;
           
        DB)
            rm "$1"
            return 0
            ;;
           
        PNG)
            rm "$1"
            moveFile "$BASE/Other/PNG" "$2" 0 "PNG"
            continue
            ;;
           
        PANO)
            rm "$1"
            moveFile "$BASE/Other/PANO" "$2" 0 "PANO"
            continue
            ;;
           
        \*)
            continue
            ;;

        DS_STORE)
            rm "$1"
            ;;

        *)
            echo Unmaped extension $EXT
            continue
            ;;

    esac

    if [ -z "$YEAR" ] ; then
        echo "Image with no YEAR"
        continue
    fi

    if [ -z "$MONTH" ] ; then
        echo "Image with no MONTH"
        continue
    fi

    if [ -z "$DAY" ] ; then
        echo "Image with no DAY"
        continue
    fi

    if [ -z "$HOUR" ] ; then
        echo "Image with no HOUR"
        continue
    fi

    if [ -z "$MINUTE" ] ; then
        echo "Image with no MINUTE"
        continue
    fi

    if [ -z "$SECOND" ] ; then
        echo "Image with no SECOND"
        continue
    fi

    TARGET="$BASE/Sorted/$CAMERA/$YEAR/$MONTH/$DAY/$YEAR$MONTH$DAY$HOUR$MINUTE$SECOND.0.$EXT"
   
    if [ -f "$TARGET" ] ; then
   
        SOURCEHASH=`md5 -r "$1" | awk '{ print $1; }'`
        TARGETHASH=`md5 -r "$BASE/Sorted/$CAMERA/$YEAR/$MONTH/$DAY/$YEAR$MONTH$DAY$HOUR$MINUTE$SECOND.0.$EXT" | awk '{ print $1; }'`

        if [ "$SOURCEHASH" == "$TARGETHASH" ] ; then
            moveDuplicateFile "$1" "$CAMERA" "$YEAR" "$MONTH" "$DAY" "$HOUR" "$MINUTE" "$SECOND" "$EXT"
        else
            moveNonDuplicateFile "$1" "$CAMERA" "$YEAR" "$MONTH" "$DAY" "$HOUR" "$MINUTE" "$SECOND" "$EXT"
        fi
    else
        moveNonDuplicateFile "$1" "$CAMERA" "$YEAR" "$MONTH" "$DAY" "$HOUR" "$MINUTE" "$SECOND" "$EXT"
    fi

#    mv -n "$1" "$TARGET"
}

processDirectory()
{
    echo "Processing dir  $1"
   
    cd "$1"

    YEAR=""
    MONTH=""
    DAY=""
    HOUR=""
    MINUTE=""
    SECOND=""
    CAMERA=""

    for FILE in * ; do
   
        if [ -d "$1/$FILE" ] ; then
            processDirectory "$1/$FILE"
#            rmdir "$1/$FILE"
        else
            processFile "$1/$FILE" "$FILE"
        fi
   
    done

    if [ -f ".DS_Store" ] ; then
        rm .DS_Store
    fi   

    cd ..
    rmdir "$1"
}

CURRENT=`pwd`

processDirectory "$CURRENT”

Trying to be a bit more social

So, I know this blog hasn’t been active in a while.  Anyone that knows me will know that I’m not necessarily that big into social media.  I like the concept, but I tend to get a bit too busy to bother posting.  Oh, and don’t even bother looking for me on Facebook.  I’m not there, at least not really.

However, given that I do have this blog and this little spot in the world, I thought I’d give it another go.  No promises for sure.