L'Isola di Niente
L'Isola di Niente » Nautilus Tips » Nautilus をスクリプトで拡張

Nautilus をスクリプトで拡張

Nautilus はスクリプトを利用して簡単に拡張できます。
検索すると bash スクリプトしか見当たりませんが Python, Gjs 等も使えます。
2016.08.29 : 内容が古くなったので多数の書き換え
2014.01.03 : Python スクリプトの Python3 化及び整理
2013.01.28 : Nautilus 3.6 仕様の追記
2012.03.04 : PyGtk コードを PyGObject に書き換え、及び細かい整理
2010.08.14 : 一部修正及び追記

利用方法

ファイルマネージャが Nautilus なら以下のディレクトリが最初からあるはずです。
# Nautilus 3.6 以降
~/.local/share/nautilus/scripts

# Nautilus 3.4 以前
~/.gnome2/nautilus-scripts
ココに実行可能パーミッションを与えたスクリプトファイルを置くだけです。
又このディレクトリにサブディレクトリを作ればメニューに展開されます。

実行パーミッションを与えるにはスクリプトに chmod +x コマンド。
GUI で個別にやりたいならコンテキストメニューからプロパティを選んで
[プログラムとして実行できる] にチェックを入れます。

環境変数として以下が用意されております。
NAUTILUS_SCRIPT_SELECTED_FILE_PATHS        # 選択中ファイルのフルパス(改行区切り)
NAUTILUS_SCRIPT_SELECTED_URIS              # 上記を URI に変換したもの
NAUTILUS_SCRIPT_CURRENT_URI                # 現在表示しているディレクトリの URI
NAUTILUS_SCRIPT_WINDOW_GEOMETRY            # [横幅]x[高さ]+[左からの座標]+[上からの座標]


# bash からは書くまでもなく $ を付けて利用
$NAUTILUS_SCRIPT_SELECTED_FILE_PATHS

# Python は os モジュールで
import os
s = os.environ["NAUTILUS_SCRIPT_SELECTED_FILE_PATHS"]

// Gjs は GLib で
const GLib = imports.gi.GLib;
let s = GLib.getenv("NAUTILUS_SCRIPT_SELECTED_FILE_PATHS");
スクリプト実行時のカレントディレクトリは実行した時に開いているディレクトリになります。

実行可能パーミッションを与えた時点で本メニュー及び右クリックメニューに追加されます。
一つ登録した後なら下画像のように「このフォルダーを開く」メニューも利用できるようになります。

img/nautilus320.png

3.6 以降はファイルを選択して右クリックしメニューを出す。
MacBook の二本指タップにも対応しています。
何も選択していないと出ません

ファイルを送る形なので普通にファイル名が起動パラメータとして取得もできます。
環境変数よりそちらのほうが都合がよい場合はそちらも使えます。
小物 GUI アプリを作ったけど組み込んだほうが便利だった場合なんかに便利です
このページの一番下がそうなった例

3.18 からはファイル名のアンダーバーをニーモニックとして使えるようになりました。
(Alt キーとアンダーバーの次のアルファベットキーで実行できる機能)

しかし並び順は以前は名前順でしたが 3.18 ではよく解らない順番になっていました。
3.20 は名前の逆順に並ぶようです。

スクリプトの例

以下は筆者が利用している例。
拡張子は不要ですがシバンは必ず書くようにしましょう(UNIX のお約束)
お好みの言語を利用して作ってください。

現在開いているディレクトリから端末を開始
#!/bin/sh

gnome-terminal

*.html や実行パーミッションを付けた *.py を一発で gedit で開く
以下ならファイル名に半角空白があっても開くことが可能
#!/bin/sh

#gedit $NAUTILUS_SCRIPT_SELECTED_FILE_PATHS
gedit "$@"

つまり Windows の「送る」みたいなランチャになる
よく使うものはどんどん登録してしまおう
#!/bin/sh

ghex "$@"

nautilus-gksu で検索してくる人がいまだにいるんですけど...
Ubuntu 使いって Mac すら使えないサルばかり、Mac は皆端末でやってるよ
つか UNIX 系に慣れると全部端末でやるようになるので筆者も使っていない
#!/bin/sh

# Fedora は beesu をインストールして
beesu gvfs-open $NAUTILUS_SCRIPT_SELECTED_FILE_PATHS

# Ubuntu (最近のバージョンは可能かどうか試していない)
#gksu gvfs-open $NAUTILUS_SCRIPT_SELECTED_FILE_PATHS

実行パーミッションを簡単に切り替える
#!/bin/sh

for name in $NAUTILUS_SCRIPT_SELECTED_FILE_PATHS
do
    if test -x $name
    then
        chmod -x $name
    else
        chmod +x $name
    fi
done 

デジカメや iPhone から画像を転送すると拡張子が大文字になっている
それらをまとめて小文字(*.jpg)に変更
シェルスクリプトに書き換えました
#!/bin/sh

for name in $NAUTILUS_SCRIPT_SELECTED_FILE_PATHS
do
    mv $name ${name%.*}.jpg
done

H.264 + aac (or mp3) の FLV コンテナ動画を MP4 コンテナに変更
Finder, Explorer でもサムネイル可能になるメリットがある
シェルスクリプトに書き換えました
#!/bin/sh

for name in $NAUTILUS_SCRIPT_SELECTED_FILE_PATHS
do
    ffmpeg -i $name -vcodec copy -acodec copy ${name%.*}.mp4
done

Google Chrome のキャッシュから指定 MIME Type のファイルを取り出す。
Youtube は随分前から駄目になりましたがニコニコ○画ではまだ使えます
拡張子を追加、開いているディレクトリにコピー。
ただサイズが大きめのファイルだとキャッシュされないようです
v51 以降は余計なヘッダが付加されるので取り払う処理を入れました
#!/usr/bin/env python3

"""
    chrome_catch (~/.local/share/nautilus/scripts)
    Copy Chrome Cache File to Nautilus Current Directry
    Google Chrome 51 Version
"""

import os
from gi.repository import GLib

FLV_FILE = "FLV".encode("utf-8")
MP4_FILE = "ftyp".encode("utf-8")

# Src Directory is Google Chrome Cache Path
src_path = os.path.expanduser("~/.cache/google-chrome/Default/Cache")
# Copy Directory is Nautilus current directory
dst_path = GLib.filename_from_uri(GLib.getenv("NAUTILUS_SCRIPT_CURRENT_URI"))[0]

ls = os.listdir(src_path)
for f in ls:
    src = os.path.join(src_path, f)
    # size < 1MB is continue:
    if os.path.getsize(src) < 1048576:
        continue
    with open(src, "rb") as o:
        data = o.read()
        data2 = data # pointer copy
        try:
            i = data.find(MP4_FILE)
            o.seek(i-4)
            with open("{0}/{1}.mp4".format(dst_path, f), "wb") as ff:
                ff.write(o.read())
        except Exception as e:
            pass
        #data2 = o.read()
        try:
            i = data2.find(FLV_FILE)
            o.seek(i)
            with open("{0}/{1}.flv".format(dst_path, f), "wb") as ff:
                ff.write(o.read())
        except Exception as e:
            pass

ディレクトリ内の無作為なファイル名画像が膨大な数に!
ということがよくあるのは筆者だけ?
とりあえずゼロ詰めな連番にしてしまおう、という場合用
#!/usr/bin/env python3

import os
import subprocess

l = os.environ["NAUTILUS_SCRIPT_SELECTED_FILE_PATHS"].split('\n')
i = 1
for n in l:
    path, name = os.path.split(n)
    while 1:
        s = "{0:03d}.jpg".format(i)
        if not os.path.isfile(s):
            subprocess.call(["mv", name, s])
            break
        i += 1

大きな画像へのリンク用の小さな画像作り(jpeg, png 限定)
生成する画像の名前は Wordpress が付ける名前っぽいサイズを付加する例
#!/usr/bin/env python3

from gi.repository import GdkPixbuf
import os

path_array = os.environ["NAUTILUS_SCRIPT_SELECTED_FILE_PATHS"].split("\n")
for filepath in path_array:
    try:
        pixbuf = GdkPixbuf.Pixbuf.new_from_file(filepath)
    except:
        continue
    # 300*300 以下で最大の大きさにする
    d_width = 300
    d_height = 300
    # GdkPixbuf のサイズ取得
    p_width = pixbuf.get_width()
    p_height = pixbuf.get_height()
    # 小さい方に合わせる計算
    width = 0
    height = 0
    if (d_width * p_height) > (d_height * p_width):
        width = p_width * d_height // p_height
        height = d_height
    else:
        width = d_width
        height = p_height * d_width // p_width
    # リサイズされた Pixbuf 作成
    smallpix = GdkPixbuf.Pixbuf.scale_simple(pixbuf, width, height, GdkPixbuf.InterpType.BILINEAR)
    # jpeg or png
    name, ext = os.path.splitext(filepath)
    ext = ext.lower()
    if ext == ".jpg" or ext == ".jpeg":
        smallpath = "{0}-{1}x{2}.jpg".format(name, width, height)
        smallpix.savev(smallpath, "jpeg", ["quality"], ["85"])
    elif ext == ".png":
        smallpath = "{0}-{1}x{2}.png".format(name, width, height)
        smallpix.savev(smallpath, "png", ["compression"], ["9"])

ffmpeg を使って動画ファイルの特定時間の箇所を切り出す
独立アプリにするより Nautilus に組み込んだほうが使いやすそうなので
Gjs にしてみました
#!/usr/bin/gjs

const Gtk = imports.gi.Gtk;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const Lang = imports.lang;
const System = imports.system;

const NumEntry = new Lang.Class({
    Name: 'NumEntry',
    Extends: Gtk.Entry,

    _init: function() {
        this.parent({
            xalign: 1.0
        });
        let buf = this.get_buffer();
        buf.connect("inserted-text", Lang.bind(this, function(buf, position, chars, n_chars) {
            if (isNaN(chars))
                buf.delete_text(position, n_chars);
        }));
    }
});

const FFCut = new Lang.Class({
    Name: 'FFCut',
    Extends: Gtk.ApplicationWindow,

    _init: function(app, basename) {
        this.parent({
            application: app,
            title: basename
        });
        // Grid
        let texts = ["Start", ":", "Stop", ":"];
        let labels = [];
        this.entries = [];
        for (let i=0; i<4; i++) {
            labels[i] = new Gtk.Label({label: texts[i]});
            this.entries[i] = new NumEntry();
        }
        let grid = new Gtk.Grid();
        grid.attach(labels[0], 0, 0, 1, 1);
        grid.attach(labels[1], 2, 0, 1, 1);
        grid.attach(labels[2], 0, 1, 1, 1);
        grid.attach(labels[3], 2, 1, 1, 1);
        grid.attach(this.entries[0], 1, 0, 1, 1);
        grid.attach(this.entries[1], 3, 0, 1, 1);
        grid.attach(this.entries[2], 1, 1, 1, 1);
        grid.attach(this.entries[3], 3, 1, 1, 1);
        // Button
        let button = new Gtk.Button({label: "Cut"});
        button.connect("clicked", Lang.bind(this, function() {
            let ss1 = Number(this.entries[0].get_text()) * 60 + Number(this.entries[1].get_text());
            let ss2 = Number(this.entries[2].get_text()) * 60 + Number(this.entries[3].get_text()) - ss1;
            let cmd = "ffmpeg -ss " + ss1 + " -i " + this.title + " -t " + ss2 + " -vcodec copy -acodec copy out_" + this.title;
            GLib.spawn_command_line_async(cmd);
        }));
        // Pack
        let vbox = new Gtk.Box({
            orientation: Gtk.Orientation.VERTICAL,
            spacing: 5
        });
        vbox.pack_start(grid, true, true, 0);
        vbox.pack_start(button, true, true, 0);
        this.add(vbox);
        this.show_all();
    }
});

const FFApp = new Lang.Class({
    Name: 'FFApp',
    Extends: Gtk.Application,

    _init: function() {
        this.parent({
            application_id: 'org.sasakima.ffcut',
            flags: Gio.ApplicationFlags.HANDLES_OPEN
        });
    },
    vfunc_open: function(files, hint) {
        let basename = files[0].get_basename();
        let w = new FFCut(this, basename);
    },
    vfunc_activate: function() {
        print("Usage: ffcut FILENAME");
    }
});

let argv = [System.programInvocationName];
ARGV.forEach(function(element) {
    if (element.indexOf("//") == -1) {
        argv.push(decodeURIComponent(escape(element)));
    } else {
        argv.push(element);
    }
});
let application = new FFApp();
application.run(argv);

// ex: ts=4 sw=4 noet ft=js

Copyright(C) sasakima-nao All rights reserved 2002 --- 2017.