Share
Trang chủ / Tất cả hướng dẫn / Lập trình windows / Viết chrome extension tải ảnh từ flickr

Viết chrome extension tải ảnh từ flickr

Hướng dẫn viết extension cho google chrome để tải các ảnh bị khóa, tải nguyên album ảnh từ flickr nhanh chóng. Extension sử dụng api do flickr cung cấp.

Mục tiêu của hướng dẫn này là giúp các bạn hiểu về cách sử dụng ajax trong google chrome extension, Sử dụng page action, Sử dụng google analytics , sử dụng API của flickr để hỗ trợ tải ảnh từ flickr.

Khi bạn tìm kiếm ảnh trên trang flickr.com, có rất nhiều ảnh đẹp mà tác giả đã tắt chức năng tải. Tuy nhiên cũng có rất nhiều cách khác nhau để “cưỡng đoạt” tấm ảnh đó.

  1. Cách thứ nhất : Nếu bạn xem mã nguồn của trang web, nhất định bạn sẽ thấy được link tải ảnh.
  2. Cách thứ hai : Sử dụng API mà Flickr cung cấp để tải ảnh.
  3. Cách thứ ba : Sử dụng thư viện của google chrome cung cấp để detect tài nguyên đang được tải về của trang web, rồi lấy ra link download ảnh.
  4. Cách thứ tư : Gửi thư cho tác giả để hỏi xin ảnh.

Trong bốn cách trên, thì cách thứ 4 là cách an toàn và lịch sự nhất, các cách còn lại thích hợp cho sự nhanh chóng, kém an toàn và là cách mà chúng ta sẽ sử dụng trong hướng dẫn này. Nhưng chúng ta đang làm hướng dẫn về lập trình, nên chúng ta sẽ chọn cách nào phù hợp với việc lập trình nhất, có thể là extension cho phép tự gửi request đến tác giả hoặc cho phép “cững đoạt” tất cả. Chúng ta cùng phân tích và chọn cách tốt nhất nhé.

Cách thứ nhất : Cách này thích hợp cho bạn tự xem mã nguồn thủ công bằng tay, không thích hợp làm extension, vì lấy mã nguồn bằng extension thì hơi mệt một xíu. Nếu muốn làm theo cách này, bạn sẽ phải inject một đoạn javascript vào mã nguồn của trang web, sao đó sẽ lấy nội dung của element chứa link.

Cách thứ hai : Đây là một cách tốt nếu mục đích của chúng cũng là để tìm hiểu luôn về cách sử dụng API trong google chrome extension, cách sử dụng API của flickr để có thể áp dụng vào các project khác. Nhưng với cách này chúng ta sẽ phải request lên server thêm một lần nữa. Bù lại, sau lần request này, chúng ta có thể lấy được nhiều kích thước khác nhau của ảnh. Vậy đây là cách chúng ta sẽ chọn.

Cách thứ 3 : Đây là một cách tốt để làm ứng dụng thành phẩm, nó nhanh hơn cách thứ 2, vì sẽ không phải gọi đến server của Flickr, nhưng bù lại, chúng ta khó lấy được nhiều kích thước khác nhau của ảnh.

Sơ lược về ý tưởng code

Trong chrome extension có hai loại action là page action và browser action, bạn nên tìm hiểu về hai khái niệm này trước, đây là tài liệu : Browser action và Page action

page action and browser action
Page action and prowser action

Plugin của chúng ta sẽ chỉ hoạt động trên tab truy cập vào Flickr. Vì vậy extension của chúng ta sẽ không show icon của extension này trên mọi tab, theo lời khuyến cáo của google : “If you want to create an icon that isn’t always visible, use a page action instead of a browser action.”. Như vậy chúng ta sẽ chọn “page action”.

Khi một tab của người dùng truy cập vào trang flick có ảnh, chúng ta sẽ show action ra. Khi người dùng ấn vào action, chúng ta show một popup, trong popup này, chúng ta cho người dùng nhiều lựa chọn về kích cỡ của ảnh để họ download.

Chúng ta hỗ trợ hai thể loại, là một ảnh riêng lẻ và một bộ ảnh.

Flickr API và tạo application trong Flickr

Bạn nên đọc tài liệu về Flickr API tại : Flickr API

Trong các API của flickr, có một số interface đòi hỏi phải xác thực người dùng, là đòi hỏi người đang sử dụng ứng dụng có API này phải xác thực là có cấp quyền cho ứng dụng, giống như khi bạn sử dụng application trên facebook, một số khác lại không. Rất may, hai interface chúng ta sử dụng là flickr.photos.getSizesflickr.photosets.getPhotos không đòi hỏi phải xác thực.

Các interface này hỗ trợ kiểu dữ liệu trả về là xml, json, jsonp hoặc php serial. Chúng ta nên chọn json hoặc jsonp cho javascript.

Tạo application và lấy api

Bạn truy cập vào địa chỉ  https://www.flickr.com/services/apps/create/noncommercial/ vaà nhập thông tin ứng dụng của bạn.

Sau khi submit, bạn quay lại trang https://www.flickr.com/services/apps , nhìn vào khung app bạn mới tạo, bạn sẽ thấy các key, lưu lại các key này, ở các bước sau bạn sẽ cần đến.

Hiện thực extension

Bạn đọc thêm tài liệu về lập trình extension tại trang của google :   Getting Started: Building a Chrome Extension

Như bạn thấy, việc lập trình extension cho google chrome tương đối đơn giản, các kiến thức cần sử dụng hầu như là kiến thức lập trình web bình thường, ngoài một số phương thức đặt biệt mà google chrome cung cấp.

Thiết lập file manifest.json

"page_action": {
 "default_icon": "images/icon_19.png",
 "default_title": "__MSG_defaultTitle__",
 "default_popup": "popup.html"
}

Đoạn code trên chúng ta đã khai báo một page action, có icon là icon_19.png, khi click vào icon này thì sẽ hiển thị file popup.html

"background": {
 "scripts": ["javascript/background.js"]
}

Khai báo một file background.js, file này sẽ được chạy khi plugin được load.

"permissions": [
 "tabs",
 "activeTab",
 "http://*.flickr.com/*",
 "https://*.flickr.com/*",
 "storage"
]

Chúng cần google chrome cấp các quyền truy cập vào tabs, tabs đang hiển thị, các trang có domain flickr, và quyền lưu trữ dữ liệu local.

"content_security_policy": "script-src 'self' https://*.google-analytics.com; object-src 'self'"

Vì lý do bảo mật, google chrome rất hạn chế các tiếp xúc trực tiếp của extension ra “thế giới văn minh”. Vì vậy chúng ta phải khai báo các nguồn script mà chúng ta cần thì google chrome mới cho phép chúng ta giao tiếp, sử dụng. Trong trường hợp này mình muốn giao tiếp với google analytics để thống kê các hành vì của người dùng.

File popup.html

Đây là phần khung của popup. Chứa mã html, trong file này, chúng ta cũng khai báo các file css, javascript mà chúng ta sử dụng giống như lập trình web bình thường.

Chúng ta cần một title cho đẹp, một container để hiển thị các ảnh và link download. Chúng ta sử dụng thư viện jquery để hỗ trợ việc lập trình được tiện hơn.

Background.js

Các bạn có thể hình dung, file background.js là một parent, có view rộng và nhìn thấy được các dữ liệu, phương thức mà file popup không nhìn thấy được. Chúng ta cần file background.js để quản lý và hỗ trợ cho popup.

Khi plugin được load lên, google chrome sẽ tạo một file _generated_background_page.html, và chèn file bakcground.js của chúng ta vào rồi chạy nói.

Để biết được khi nào người dùng truy cập vào trang flickr, chúng ta lắng nghe sự kiện updated của tab, sự kiện này sẽ trả về cho chúng ta thông tin của tab.

chrome.tabs.onUpdated.addListener(function(id, info, tab){
    var parser = document.createElement('a');
    parser.href = tab.url;
    if(parser.hostname =="www.flickr.com") {
        var pathnames = parser.pathname.split('/');
        if( isElEqual(pathnames[1], "photos"))
        {
            if( isElEqual(pathnames[3], "sets") ) {
                if(isNumber(pathnames[4])) {
                    chrome.pageAction.show(tab.id);
                }
            }
            else
            {
                if(isNumber(pathnames[3])) {
                    chrome.pageAction.show(tab.id);
                }
            }
        }
    }
});

Khi có url, chúng ta sẽ phân tích, để parse url, chúng ta tạo một anchor element, element này sẽ tự động parse và trả về cho chúng ta các thuộc tính:

var parser = document.createElement('a');
parser.href = "http://example.com:3000/pathname/?search=test#hash";

parser.protocol; // => "http:"
parser.hostname; // => "example.com"
parser.port;     // => "3000"
parser.pathname; // => "/pathname/"
parser.search;   // => "?search=test"
parser.hash;     // => "#hash"
parser.host;     // => "example.com:3000"

Nếu người dùng đang truy cập trang flickr thì parser.hostname có giá trị là “www.flickr.com”. Lúc đó chúng ta sẽ tách parser.pathname ra để biết người dùng đang ở trang nào. Nếu là trang có chứa ảnh, thì chúng ta mới tiếp tục.

var pathnames = parser.pathname.split('/');

Sau lênh trên, pathnames sẽ chứa các path được tách ra.  Nếu pathnames[1] có giá trị là “photos” thì ta mới tiếp tục, vì chỉ ở những trang https://www.flickr.com/photos mới chưa ảnh cần download.

if( isElEqual(pathnames[1], "photos"))

. Trong code sử dụng hàm isElEqual(array, index, value). Hàm này được định nghĩ nhằm xác định xem ở vị trí index có phải là giá trị value không :

function isElEqual(value1, value2)
{
    if( typeof value1 != 'undefined' )
    {
        if( value1 ==  value2)
        {
            return true;
        }
    }
    return false
}

Phân tích url của flick, chúng ta thấy nếu pathnames[4] có giá trị là “sets”, thì người dung đang truy cập vào giao diện của các bộ ảnh, nếu sau “sets/” là một số, thì người dùng đang xem 1 bộ ảnh có id là số đó. Nếu sau “sets/” là trống, thì người dùng đang xem danh sách các bộ ảnh.

if( isElEqual(pathnames[3], "sets") ) {
    if(isNumber(pathnames[4])) {
        chrome.pageAction.show(tab.id);
    }
}
else
{
    if(isNumber(pathnames[3])) {
        chrome.pageAction.show(tab.id);
    }
}

Nếu pathnames[4] không phải là “sets” và pathnames[3] là một số, thì người dùng đang xem một bức ảnh, và pathnames[3] chính là id của ảnh đó. Trong cả hai trường hợp trên, chúng ta đều sẽ show icon ra.

Sẽ làm gì khi action page show ra ?

Khi người dùng ấn vào icon trên actionbar, file popup.html sẽ được mở lên, và show ra dạng popup.

chrome page action popup
popup showing

Đối với file popup này, chúng ta lập trình như một trang web html bình thường.

File popup.html

<html>
    <head>
        <link href="style/popup.css" rel="stylesheet" type="text/css">
        <meta charset="UTF-8" />
        <script type="text/javascript" src="javascript/jquery-1.7.2.min.js"></script>
        <script type="text/javascript" src="javascript/popup.js"></script>
    </head>
    <body>
        <div id="main-wrap">
            <div class="top-wrap">
                <div class="inside-layout">
                    <div class="app-logo"></div>
                    <div class="app-name">Download Flickr Image</div>
                    <div class="clearfix"></div>
                </div>
            </div>
            <div class="content-wrap">
                <div id="loading">Loading...</div>
                <div id="imgContainer">
                </div>
                <div class="clearfix"></div>
            </div>
        </div>
    </body>
</html>

popup.js

Khi các dom được tạo xong, phương thức ready() của jquery sẽ chạy, lúc đó chúng ta cũng xác định giống như bên file background.js để các định url người dùng truy cập là gì. Nhưng trong file popup thì không handle được sự kiện updated. Muốn lấy url của tab, phải dử dụng chrome.tabs.query().

chrome.tabs.query({
    active: true,
    windowId: chrome.windows.WINDOW_ID_CURRENT
}, function (tabs) {
    // and use that tab to fill in out title and url
    var tab = tabs[0];
    processImage(tab.url);
});

hàm dưới đây sẽ xác định xem người dùng đang xem album hay xem ảnh lẻ

function processImage(url) {

    var parser = document.createElement('a');
    parser.href = url;
    var pathnames = parser.pathname.split('/');
    // Set
    if (isElEqual(pathnames[3], "sets")) {
        if ((localStorage["lastID"] == pathnames[4]) && ( localStorage["lastContent"] )) {
            restoreLast();
        }
        else {
            localStorage["lastID"] = pathnames[4];
            getPhotoset({id: pathnames[4], page: 1});
        }
    }
    // Photo
    else if (isNumber(pathnames[3])) {
        if (( localStorage["lastID"] == pathnames[3] ) && ( localStorage["lastContent"] )) {
            restoreLast();
        }
        else {
            localStorage["lastID"] = pathnames[3];
            getPhoto({id: pathnames[3]});
        }
    }
}

Việc tiếp theo chúng ta kiểm tra, là xem id mới này có trùng với id chúng ta cache hay không, nếu trùng, thì không cần phải request mới nữa, mà chỉ cần lấy trong cache ra, nếu không trùng, thì request server và lưu vài cache.

Trong ready(), chúng ta cũng sẽ handle sự kiện ajaxComplete. Sự kiện này sẽ được gọi mỗi khi có một ajax request hoàn thành. Lúc đó, chúng ta sẽ trừ totalRequets đi một đơn vị, và nếu request = 0 thì sẽ lưu nội dung vào cache.

$(document).ajaxComplete(function() {
    totalRequest--;
    if(totalRequest == 0)
    {
        localStorage["lastContent"] = $("#imgContainer").html();
    }
});

Hàm request lên server

function sendRequest(option) {
    $.ajax({
        url: apiUrl + option.query + apiKeyParam,
        dataType: "json",
        global : option.global,
        success: function (response) {
            if (option.onSuccess && typeof(option.onSuccess) === "function") {
                option.onSuccess(response);
            }
        },
        fail: function () {
            if (option.onFail && typeof(option.onFail) === "function") {
                option.onFail(response);
            }
        }

    });
}

Tham số option :

  • query : là query sẽ request lên server, bạn sẽ hiểu hơn khi xem các hàm phía dưới
  • global : xác định xem sau khi request này complete, có gọi đến sự kiện ajaxComplete hay không.
  • onSuccess, onFail : hai callback function.

Hàm xử lý lấy ảnh của album:

function getPhotoset(option) {
    var query = 'flickr.photosets.getPhotos&per_page=500&page=' + option.page + '&photoset_id=' + option.id;
    sendRequest({
        query: query,
        global:false,
        onSuccess: function (response) {
            var html = "";
            if (response.stat == "ok") {
                if (response.photoset.photo) {
                    $.each(response.photoset.photo, function (index, photo) {
                        getPhoto({id: photo.id});
                    });
                }
                if (option.page < response.photoset.pages) {
                    getPhotoset({id: option.id, page: (option.page+1)});
                }
            }
        }
    });
}

Nếu có lỗi xảy ra, server sẽ hồi đáp “stat” là fail, và nội dung lỗi nằm ở “message”.  Nếu thành công, hàm này sẽ nhận về một danh sách các photo, các photo này bao gồm ID, sau đó phải gọi hàm getPhoto(id) để lấy link của từng photo. Số lượng image tối đa mà flickr cho phép trong một lần get là 500, vì kết quả sẽ bị paging, nên ta sẽ kiểm tra, nếu page hiện tại nhỏ hơn tổng số page, thì gọi đệ quy để tải page tiếp theo.

function getPhoto(option) {
    totalRequest++;
    var query = 'flickr.photos.getSizes&photo_id=' + option.id;
    sendRequest({
        query: query,
        global:true,
        onSuccess: function (response) {
            if (response.stat == "ok") {
                if (response.sizes) {
                    var container = $("#imgContainer");
                    var html = '<div class="row">' + generatePhotoLink(response.sizes.size) + '</div>';
                    container.append(html);
                }
            }
        }
    });
}

Hàm này gửi request lên server, nhận lại danh sách các ảnh kèm theo link và các kích thước. Nó sẽ gọi hàm generatePhotoLink() để định dạng các link này thành html và show ra trang popup.html.

Để tạo link tự động save, thì ta thêm “_d” và sau link ảnh bằng hàm :

function getPhotoURL(url) {
    var urlArr = url.split(".");
    urlArr[urlArr.length - 2] += "_d";
    return urlArr.join(".");

Kết luận

Như vậy bạn đã tạo thành công một extension cho google chrome, Chúng tôi không làm những hướng dẫn quá cơ bản, hầu như những gì cơ bản nhất đều đã được mô tả rất cụ thể tại các trang của chính google. Vì vậy bạn có thể tự tìm hiểu.

Có khá nhiều điều mà extension này chưa phát triển. Sau đây là một vài gợi ý : Vì khi download nguyên album, mỗi lần click vào một ảnh là popup sẽ ẩn đi, khá bất tiện, lại chưa có chức năng download nguyên album một lần, có thể cải tiến bằng cách sử dụng api “app lauch” để tạo một cửa sổ mới.