PyGTK Threaded Nautilus Filebot

As a heavy user of Nautilus and Filebot, i decided to integrate the two.
Following on from my previous article on column providers for nautilus, here as the additional functionality for nautilus filebot integration.
Its a basic threaded extension; for the most basic and most used operation, “Strict file renaming”.

It solves 95% of my usage scenarios. To find out how to use the code, see the article https://fio.ie/python-column-provider-nautilus/

Github: https://github.com/dmzoneill/filebot-nautilus

FileBot Nautilus

The code

#!/usr/bin/python

# dave@fio.ie

import os
import urllib
import logging
import re
import threading

import gi
gi.require_version('Nautilus', '3.0')
gi.require_version('Gtk', '3.0')

from gi.repository import Nautilus, GObject, Gtk, Gdk, GLib, GdkPixbuf
from hachoir_core.error import HachoirError
from hachoir_core.stream import InputIOStream
from hachoir_parser import guessParser
from hachoir_metadata import extractMetadata
from subprocess import Popen, PIPE

GObject.threads_init()


class VideoMetadataExtension(GObject.GObject, Nautilus.ColumnProvider, Nautilus.MenuProvider, Nautilus.InfoProvider):

  def __init__(self):
  
    logging.basicConfig(filename='/tmp/VideoMetadataExtension.log',level=logging.DEBUG)
    
    self.videomimes = [
      'video/x-msvideo',
      'video/mpeg',
      'video/x-ms-wmv',
      'video/mp4',
      'video/x-flv',
      'video/x-matroska'
    ]
    
    self.win = None


  def get_columns(self):
  
    return (
      Nautilus.Column(name="NautilusPython::video_width_columnn",attribute="video_width",label="Width",description="Video width"),
      Nautilus.Column(name="NautilusPython::video_height_columnn",attribute="video_height",label="Height",description="Video height"),
    )


  def update_file_info_full(self, provider, handle, closure, file_info):
  
    filename = urllib.unquote(file_info.get_uri()[7:]) 
    video_width = ''
    video_height = ''
    name_suggestion = ''

    file_info.add_string_attribute('video_width', video_width)
    file_info.add_string_attribute('video_height', video_height)
    file_info.add_string_attribute('name_suggestion', name_suggestion)  
  
    if file_info.get_uri_scheme() != 'file':
      logging.debug("Skipped: " + filename)
      return Nautilus.OperationResult.COMPLETE
      
    for mime in self.videomimes:
      if file_info.is_mime_type(mime):
        GObject.idle_add(self.get_video_metadata, provider, handle, closure, file_info)
        logging.debug("in Progress: " + filename)
        return Nautilus.OperationResult.IN_PROGRESS
         
    logging.debug("Skipped: " + filename)
    
    return Nautilus.OperationResult.COMPLETE


  def get_file_items_full(self, provider, window, files):
  
    for mime in self.videomimes:
      for file in files:
        if file.get_uri_scheme() == 'file' and file.is_mime_type(mime):
      
          top_menuitem = Nautilus.MenuItem(name='NautilusPython::Filebot', label='Filebot', tip='Filebot renamer')

          submenu = Nautilus.Menu()
          top_menuitem.set_submenu(submenu)

          filebot_tvdb_menuitem = Nautilus.MenuItem(name='NautilusPython::FilebotRenameTVDB', label='Filebot TVDB', tip='Fetch names from TVDB')
          filebot_tvdb_menuitem.connect('activate', self.filebot_activate_cb, files, 'tvdb')
          submenu.append_item(filebot_tvdb_menuitem)

          filebot_moviedb_menuitem = Nautilus.MenuItem(name='NautilusPython::FilebotRenameMoviewDB', label='Filebot MovieDB', tip='Fetch names from MovieDB')
          filebot_moviedb_menuitem.connect('activate', self.filebot_activate_cb, files, 'moviedb')
          submenu.append_item(filebot_moviedb_menuitem)

          return top_menuitem,


  def filebot_activate_cb(self, menu, files, source):
  
    self.win = FileBotWindow(self, source, files)
    self.win.connect("delete-event", Gtk.main_quit)
    self.win.show_all()
    Gtk.main()
        

  def get_video_metadata(self, provider, handle, closure, file_info):
  
    video_width = ''
    video_height = ''
    name_suggestion = ''
  
    filename = urllib.unquote(file_info.get_uri()[7:])  
    filelike = open(filename, "rw+")
    
    try:
      filelike.seek(0)
    except (AttributeError, IOError):
      logging.debug("Unabled to read: " + filename)
      Nautilus.info_provider_update_complete_invoke(closure, provider, handle, Nautilus.OperationResult.FAILED)
      return False

    stream = InputIOStream(filelike, None, tags=[])
    parser = guessParser(stream)

    if not parser:
      logging.debug("Unabled to determine parser: " + filename)
      Nautilus.info_provider_update_complete_invoke(closure, provider, handle, Nautilus.OperationResult.FAILED)
      return False

    try:
      metadata = extractMetadata(parser)
    except HachoirError:
      logging.debug("Unabled to extract metadata: " + filename)
      Nautilus.info_provider_update_complete_invoke(closure, provider, handle, Nautilus.OperationResult.FAILED)
      return False
          
    if metadata is None:
      logging.debug("Metadata None: " + filename)
      Nautilus.info_provider_update_complete_invoke(closure, provider, handle, Nautilus.OperationResult.FAILED)
      return False

    matchObj = re.search( r'Image width: (.*?) pixels', str(metadata), re.M|re.I)

    if matchObj:
       video_width = matchObj.group(1)
        
    matchObj = re.search( r'Image height: (.*?) pixels', str(metadata), re.M|re.I)

    if matchObj:
       video_height = matchObj.group(1)

    file_info.add_string_attribute('video_width', video_width)
    file_info.add_string_attribute('video_height', video_height)
    file_info.add_string_attribute('name_suggestion', name_suggestion)   
    
    logging.debug("Completed: " + filename)
    
    file_info.invalidate_extension_info()
    
    Nautilus.info_provider_update_complete_invoke(closure, provider, handle, Nautilus.OperationResult.COMPLETE)
    
    return False
    

class FileBotWindow(Gtk.Window):

  def __init__(self, videoMetadataExtension, source, files):
    
    self.todo = len(files)
    self.processing = 0
    self.files = files
    self.source = source
    self.quit = False
    
    self.videoMetadataExtension = videoMetadataExtension

    Gtk.Window.__init__(self, title="Filebot operation")
    self.set_size_request(200, 100)
    self.set_border_width(10)
    self.set_type_hint(Gdk.WindowTypeHint.DIALOG)

    vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
    self.add(vbox)

    self.working_label = Gtk.Label(label="Working")
    vbox.pack_start(self.working_label, True, True, 0)

    self.button = Gtk.Button(label="Cancel")
    self.button.connect("clicked", self.on_button_clicked)
    vbox.pack_start(self.button, True, True, 0)
    
    #GObject.timeout_add_seconds(1, self.process, files, source)
    self.update = threading.Thread(target=self.process)
    self.update.setDaemon(True)
    self.update.start()
             
        
  def process(self):
 
    for file in self.files:
      if file.get_uri_scheme() != 'file':
        continue
  
      filename = urllib.unquote(file.get_uri()[7:])
      self.filebot_process(filename)

    
  def on_button_clicked(self, widget):
    self.quit = True  
    self.close()
    

  def filebot_process(self, filename): 
    
    if self.quit == True:
      return
    
    self.processing = self.processing + 1
    text = "Processing (" + str(self.processing) + "/" + str(self.todo) + ") " + os.path.basename(filename)
    GObject.idle_add( self.working_label.set_text, text, priority=GObject.PRIORITY_DEFAULT )
        
    p = Popen(['filebot', '-rename', filename,'--db', self.source], stdin=PIPE, stdout=PIPE, stderr=PIPE)
    output, err = p.communicate(b"input data that is passed to subprocess' stdin")
    rc = p.returncode

    if self.processing == self.todo:
      GObject.idle_add( self.close, priority=GObject.PRIORITY_DEFAULT )

Python column provider for Gnome Files (Nautilus)

Below is an example of a python column provider for nautilus.
Identifying poor quality video files 480p, 720p … is difficult in nautilus at a glance.

You need to open the file properties to view the video metadata.
While there are a number of extensions out there for EXIF etc, i decided to implement my own.
Originally i tried to use MediaInfo, but it was far too slow. This is what i ended up with.

To activate it, just login and logout, or kill nautilus and restart it.

preview nautilus

Create target directory and file

mkdir -vp ~/.local/share/nautilus-python/extensions
touch ~/.local/share/nautilus-python/extensions/VideoMetaData.py

Install python libraries

pip install hachoir-metadata
pip install hachoir-parser
pip install hachoir-core

Add this to ~/.local/share/nautilus-python/extensions/VideoMetaData.py

#!/usr/bin/python

# dave@fio.ie

import os
import urllib
import logging
import re

import gi
gi.require_version('Nautilus', '3.0')
gi.require_version('Gtk', '3.0')

from gi.repository import Nautilus, GObject, Gtk, Gdk, GdkPixbuf
from hachoir_core.error import HachoirError
from hachoir_core.stream import InputIOStream
from hachoir_parser import guessParser
from hachoir_metadata import extractMetadata


class VideoMetadataExtension(GObject.GObject, Nautilus.ColumnProvider, Nautilus.MenuProvider, Nautilus.InfoProvider):

  def __init__(self):
    logging.basicConfig(filename='/tmp/VideoMetadataExtension.log',level=logging.DEBUG)
    pass


  def get_columns(self):
    return (
      Nautilus.Column(name="NautilusPython::video_width_columnn",attribute="video_width",label="Width",description="Video width"),
      Nautilus.Column(name="NautilusPython::video_height_columnn",attribute="video_height",label="Height",description="Video height"),
    )


  def update_file_info_full(self, provider, handle, closure, file_info):
  
    filename = urllib.unquote(file_info.get_uri()[7:]) 
    video_width = ''
    video_height = ''
    name_suggestion = ''

    file_info.add_string_attribute('video_width', video_width)
    file_info.add_string_attribute('video_height', video_height)
    file_info.add_string_attribute('name_suggestion', name_suggestion)  
  
    if file_info.get_uri_scheme() != 'file':
      logging.debug("Skipped: " + filename)
      return Nautilus.OperationResult.COMPLETE
      
    videomimes = [
      'video/x-msvideo',
      'video/mpeg',
      'video/x-ms-wmv',
      'video/mp4',
      'video/x-flv',
      'video/x-matroska'
    ]
    
    for mime in videomimes:
      if file_info.is_mime_type(mime):
        GObject.idle_add(self.get_video_metadata, provider, handle, closure, file_info)
        logging.debug("in Progress: " + filename)
        return Nautilus.OperationResult.IN_PROGRESS
         
    logging.debug("Skipped: " + filename)
    
    return Nautilus.OperationResult.COMPLETE


  def get_video_metadata(self, provider, handle, closure, file_info):
  
    video_width = ''
    video_height = ''
    name_suggestion = ''
  
    filename = urllib.unquote(file_info.get_uri()[7:])  
    filelike = open(filename, "rw+")
    
    try:
      filelike.seek(0)
    except (AttributeError, IOError):
      logging.debug("Unabled to read: " + filename)
      Nautilus.info_provider_update_complete_invoke(closure, provider, handle, Nautilus.OperationResult.FAILED)
      return False

    stream = InputIOStream(filelike, None, tags=[])
    parser = guessParser(stream)

    if not parser:
      logging.debug("Unabled to determine parser: " + filename)
      Nautilus.info_provider_update_complete_invoke(closure, provider, handle, Nautilus.OperationResult.FAILED)
      return False

    try:
      metadata = extractMetadata(parser)
    except HachoirError:
      logging.debug("Unabled to extract metadata: " + filename)
      Nautilus.info_provider_update_complete_invoke(closure, provider, handle, Nautilus.OperationResult.FAILED)
      return False
          
    if metadata is None:
      logging.debug("Metadata None: " + filename)
      Nautilus.info_provider_update_complete_invoke(closure, provider, handle, Nautilus.OperationResult.FAILED)
      return False

    matchObj = re.search( r'Image width: (.*?) pixels', str(metadata), re.M|re.I)

    if matchObj:
       video_width = matchObj.group(1)
        
    matchObj = re.search( r'Image height: (.*?) pixels', str(metadata), re.M|re.I)

    if matchObj:
       video_height = matchObj.group(1)

    file_info.add_string_attribute('video_width', video_width)
    file_info.add_string_attribute('video_height', video_height)
    file_info.add_string_attribute('name_suggestion', name_suggestion)   
    
    logging.debug("Completed: " + filename)
    
    file_info.invalidate_extension_info()
    
    Nautilus.info_provider_update_complete_invoke(closure, provider, handle, Nautilus.OperationResult.COMPLETE)
    
    return False

Namecheap DDNS Updates via DD-WRT

DD-WRT has limit support for different DDNS update mechanisms.
A simple workaround is the following script.

Login to your router and go to Administration > Commands

Add the following to the startup script, modifying it for your details

cat < /tmp/namecheap.sh
KEY="YOUR NAME CHEAP DIRTY LONG KEY"
DDNS_DOMAIN="feeditout.com"
DDNS_HOST="dave-pc"
IP=\$(curl ifconfig.co)

curl -s -o temp.html "http://dynamicdns.park-your-domain.com/update?domain=\$DDNS_DOMAIN&password=\$KEY&host=\$DDNS_HOST&ip=\$IP"
EOF

chmod +x /tmp/namecheap.sh
/tmp/namecheap.sh

Finally, go to the Administration > Management and add a crontab to update it hourly.

0 */1 * * * /tmp/namecheap.sh

Enjoy 😉


DD-WRT with CA Signed certificate, bundle and key

Surprisingly enough, finding new documentation on DD-WRT, with a custom certificate, is difficult.
Yes, there is information around, however it’s date years old which doesn’t inspire a lot of confidence.

Login to your DD-WRT front end.
Go to Management -> commands.

In the commands box, paste your keys like so.
Finally at the bottom choose ‘save startup’ and your are done.
If you are concerned about it going wrong, you can run it interactively on the console first to test 🙂

Enjoy

#!/bin/sh 

cat < /tmp/key.pem
-----BEGIN RSA PRIVATE KEY-----
## YOUR PRIVATE KEY HERE ##
-----END RSA PRIVATE KEY-----
EOF

cat < /tmp/cert.pem
-----BEGIN CERTIFICATE-----
## YOUR CERTIFICATE HERE ##
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
## YOUR INTERMEDIATE CERTIFICATE HERE ##
-----END CERTIFICATE-----
EOF

mount -o bind /tmp/cert.pem /etc/cert.pem 
mount -o bind /tmp/key.pem /etc/key.pem 

stopservice httpd 
startservice httpd

Golang script to pull bandwidth usage from digiweb

I wrote the following script to pull broadband usage form digiweb.

Its fairly hilarious that digiweb are so badly organized.
They are still using smart telecom forums for customers to access pertinent data.
Furthermore the connection is http rather than https.

Here’s to passing un-encrypted passwords over the wire! 🙂

Digiweb figures are exactly 10% more that what DD-WRT reports. Interesting observation 😉

Enjoy 😉

package main

import (
    "fmt"
    "strconv"
    "net/http"
    "net/http/cookiejar"
    "crypto/tls"
    "io/ioutil"
    "crypto/md5"
    "encoding/hex"
    "net/url"
    "strings"
    "regexp"
)

var _smart_username = "username"
var _smart_password = "password"
var _modem_username = "0123456789"
var _modem_password = "password"
var _forum_endpoint = "http://support.smarttelecom.ie/forums/login.php?do=login"
var _usage_endpoint = "http://support.smarttelecom.ie/forums/smart_usage"

func GetMD5Hash(text string) string {
    hasher := md5.New()
    hasher.Write([]byte(text))
    return hex.EncodeToString(hasher.Sum(nil))
}

func getusage() {
    tr := &http.Transport {
        TLSClientConfig: &tls.Config { InsecureSkipVerify: true },
    }
    
    cookieJar, _ := cookiejar.New(nil)

    client1 := &http.Client { Transport: tr, Jar: cookieJar }
    
    form1 := url.Values{}
    form1.Add( "vb_login_username", _smart_username )
    form1.Add( "vb_login_password", "" )
    form1.Add( "s", "" )
    form1.Add( "securitytoken", "guest" )
    form1.Add( "do", "login" )
    form1.Add( "vb_login_md5password", GetMD5Hash( _smart_password ) )
    form1.Add( "vb_login_md5password_utf", GetMD5Hash( _smart_password ) )
    
    //fmt.Printf("%s", GetMD5Hash(_smart_password))
    
    req1, err := http.NewRequest( "POST", _forum_endpoint, strings.NewReader(form1.Encode()) )
    req1.Header.Add( "Content-Type", "application/x-www-form-urlencoded" )
    
    resp1, err := client1.Do( req1 )
    defer resp1.Body.Close()

    if err != nil {
        fmt.Printf( "Error : %s", err)
    }

    if resp1.StatusCode != 200 {
        fmt.Printf( "Error code: %s", strconv.Itoa( resp1.StatusCode ) )
    }
    
            
    client2 := &http.Client { Transport: tr, Jar: cookieJar }
    
    form2 := url.Values{}
    form2.Add( "user", _modem_username )
    form2.Add( "pass", _modem_password )
    form2.Add( "submit", "SUBMIT" )
    
    req2, err := http.NewRequest( "POST", _usage_endpoint, strings.NewReader(form2.Encode()) )
    req2.Header.Add( "Content-Type", "application/x-www-form-urlencoded" )
    
    resp2, err := client2.Do( req2 )
    defer resp2.Body.Close()
    
    if err != nil {
        fmt.Printf( "Error : %s", err)
    }

    if resp1.StatusCode != 200 {
        fmt.Printf( "Error code: %s", strconv.Itoa( resp1.StatusCode ) )
    }    

    bodyBytes2, err := ioutil.ReadAll( resp2.Body )
    if err != nil {
        fmt.Printf( "Error : %s", err )
    }
    
    //fmt.Printf( "%s\n", bodyBytes2 )
    
    re := regexp.MustCompile( "(?s)(.*)
" ) matches := re.FindAllString( string(bodyBytes2), -1 ) for i := len( matches )-1; i >= 0; i-- { reinout := regexp.MustCompile( "(?s)(.*?)" ) inout := reinout.FindAllString( matches[i] , -1 ) //fmt.Printf( "%v\n", inout[0][4:len(inout[0])-5] ) //fmt.Printf( "%v\n", inout[1][4:len(inout[1])-5] ) //fmt.Printf( "%v\n", inout[2][4:len(inout[2])-5] ) //fmt.Printf( "%v\n", inout[3][4:len(inout[3])-5] ) //fmt.Printf( "%v\n", inout[4][4:len(inout[4])-5] ) //fmt.Printf( "%v\n", inout[5][4:len(inout[5])-5] ) //fmt.Printf( "%v\n", inout[6][4:len(inout[6])-5] ) //fmt.Printf( "%v\n", inout[7][4:len(inout[7])-5] ) //fmt.Printf( "%v\n", inout[8][4:len(inout[8])-5] ) //fmt.Printf( "%v\n", inout[9][4:len(inout[9])-5] ) //fmt.Printf( "%v\n", inout[10][4:len(inout[10])-5] ) i := strings.Index(inout[11], "(") + 1 fmt.Printf( "%v\n", inout[11][i:len(inout[11])-10] ) } } func main() { getusage() }