Echando código: Respaldando las fotografías de Flickr usando Java y XML-RPC

Ya en otra oportunidad les habia mostrado como ver la lista de fotos en Flickr utilizando Java y XML-RPC; El principio de bajarse las imagenes es muy sencillo:

  1. Obtener el NSID de flickr, dada la dirección de correo
  2. Buscar la información de todas las imagenes asociadas a este NSID y construir un URL
  3. Por cada imagen, bajarsela de Flickr usando el URL de el paso anterior
  4. Repetir hasta que no hayan más imagenes.

La primera versión hace el trabajo:

[josevnz@localhost FlickrBackup]$ time java -jar /home/josevnz/java/FlickrBackup/dist/FlickrBackup-1.0.jar disposablehero3000-flickr3@yahoo.com password /home/josevnz/tmp/fotos
Connecting as: disposablehero3000-flickr3@yahoo.com
Pictures found: 53

real 1m52.368s
user 0m4.103s
sys 0m0.642s

Me toma casi dos minutos bajarme 53 fotos, asi que ¿Como lo puedo hacer más rápido?; Aparte de tener un buffer más grande para las imagenes (escritura / lectura) se me ocurre que puedo crear más ‘threads’, asumiendo que no me importa gastar todo mi ancho de banda bajandome imagenes, además de crear un montón de procesos en mi computadora. Cambiando solamente la forma en como guardo los datos (buffer más grande):

Connecting as: disposablehero3000-flickr3@yahoo.com
Pictures found: 53

real 1m42.083s
user 0m7.394s
sys 0m0.696s
[josevnz@localhost FlickrBackup]$

No se ganó mucho. Pero quizas pudiera utilizar una técnica comunmente utilizada en los navegadores, la cual es utilizar varias hebras para traerse los recursos a través de lared, en este caso mis fotos. Para utilizar ‘threads’ debo hacer un cambio no tan trivial en la aplicación; Esta deberá controlar cuantas hebras son creadas, y deberá esperar a que todas ellas terminen antes de matar a la hebra principal. En resumen la estrategia es la siguiente:

  1. Cree no más de ‘n’ hebras de ejecución simultaneamente.
  2. Cada hebra se baja una foto en particular y le reporta a la clase controladora como le fué en el proceso.
  3. Al final la clase controladora indica como le fué a cada una de las hebras.

Cualquiera que le diga que utilizar ‘Threads’ es fácil le está mintiendo. Si bien vienen incluidas con el lenguaje, lo cual hace su uso más fácil, no subestime sus potenciales problemas de mantenimiento, además de que no todos los programas se benefician de esta técnica (aqui estamos asumiendo que bajarse una imagen de la red es mucho más lento que generar otra hebra de ejecución para hacer lo mismo).

Primero el código de la clase que se baja las imagenes:

   1:package com.blogspot.elangelnegro.flickr;
2:
3:import java.net.URL;
4:
5:import java.io.IOException;
6:import java.io.ByteArrayInputStream;
7:import java.io.File;
8:import java.io.FileOutputStream;
9:import java.io.BufferedOutputStream;
10:import java.io.InputStream;
11:import java.net.MalformedURLException;
12:
13:import java.util.concurrent.CountDownLatch;
14:import java.util.concurrent.BrokenBarrierException;
15:
16:/**
17: * This class downloads a image from a given URL.
18: * For more information, please check the following URLS:
19: * <ul>
20: * <li><a href="http://java.sun.com/j2se/1.5.0/docs/api/">Java API</a>
21: * <li><a href="http://java.sun.com/j2se/1.5.0/docs/guide/2d/spec/j2d-bookTOC.html">Programmer's Guide to the JavaTM 2D API</a>
22: * </ul>
23: * License: GPL
24: * Blog: http://elangelnegro.blogspot.com
25: * @author Jose Vicente Nunez Zuleta
26: * @version 0.1 - 04/03/2005
27: */
28:public final class Download implements Runnable {
29:
30: private String server;
31: private String id;
32: private String secret;
33: private String title;
34: private File dir;
35: private URL url;
36: private boolean downloadStatus;
37: private CountDownLatch startSignal;
38: private CountDownLatch endSignal;
39:
40: /*
41: * Buffer size for network reading
42: */
43: private static final int BUFFER_SIZE = 819200;
44:
45: private static final NullPointerException NULLEXCEPTION = new NullPointerException();
46:
47: /**
48: * Class constructor
49: * @param server server name where the picture is stored
50: * @param secret secret that defines the image
51: * @param title Title given by the user to the image
52: * @param dir Local directory where the image will be stored
53: * @param barrier Barrier object used to notify the controler than this thread finished attempting downloading the resource
54: * @throws MalformedURLException
55: * @throws NullPointerException If any required parameter is missing. All of them are
56: */
57: public Download(String server, String id, String secret, String title, File dir, CountDownLatch startSignal, CountDownLatch endSignal) throws MalformedURLException {
58: if (server == null) {
59: throw NULLEXCEPTION;
60: }
61: this.server = server;
62: if (id == null) {
63: throw NULLEXCEPTION;
64: }
65: this.id = id;
66: if (secret == null) {
67: throw NULLEXCEPTION;
68: }
69: this.secret = secret;
70: if (title == null) {
71: throw NULLEXCEPTION;
72: }
73: this.title = title;
74: if (dir == null) {
75: throw NULLEXCEPTION;
76: }
77: this.dir = dir;
78: if (startSignal == null) {
79: throw NULLEXCEPTION;
80: }
81: this.startSignal = startSignal;
82: if (endSignal == null) {
83: throw NULLEXCEPTION;
84: }
85: this.endSignal = endSignal;
86: // Construct the actual URL
87: url = new URL("http://photos" + server + ".flickr.com/" + id + "_" + secret + "_o.jpg");
88: downloadStatus = false;
89: }
90:
91: /**
92: * Tell if this class was able to download the resource.
93: * It will return false if the method getPhoto has not been called
94: * @see #getPhoto
95: * @returns boolean
96: */
97: public boolean getStatus() {
98: return downloadStatus;
99: }
100:
101: /**
102: * Download the given picture into the local filesystem
103: * @throws IOException Reading / writting the image data
104: */
105: private void getPhoto() throws IOException {
106: /*
107: * Grab all the picture metadata information. It will be required if we want to upload the picture back
108: * to Flickr.
109: * Check: http://www.flickr.com/services/api/flickr.photos.getInfo.html
110: */
111: BufferedOutputStream image = null;
112: InputStream data = null;
113: byte [] buffer = null;
114: try {
115: // Prepare a file for writting
116: image = new BufferedOutputStream(
117: new FileOutputStream(
118: dir.getCanonicalPath() +
119: System.getProperty("file.separator") +
120: id +
121: "_" +
122: secret +
123: "_o.jpg"
124: )
125: );
126: data = url.openStream();
127: int bytes = -1;
128: buffer = new byte[BUFFER_SIZE];
129: // Save the image
130: while( (bytes = data.read(buffer, 0, buffer.length)) != -1) {
131: image.write(buffer, 0, bytes);
132: }
133: if (image != null) {
134: image.flush();
135: }
136: downloadStatus = true;
137: } catch (IOException ioexp) {
138: throw ioexp;
139: } finally {
140: buffer = null;
141: try {
142: if (image != null) {
143: image.close();
144: }
145: } catch (IOException ioexp) {
146: ioexp.printStackTrace();
147: }
148:
149: try {
150: if (data != null) {
151: data.close();
152: }
153: } catch (IOException ioexp) {
154: ioexp.printStackTrace();
155: }
156: }
157: }
158:
159: /**
160: * This method is called by the Thread manager
161: */
162: public void run () {
163: try {
164: // Wait to be awaken...
165: startSignal.await();
166: // Wrap the call to download the photo
167: getPhoto();
168: endSignal.countDown();
169: } catch (InterruptedException intExp) {
170: intExp.printStackTrace();
171: } catch (Exception exp) {
172: exp.printStackTrace();
173: } finally {
174: // Don't do anything here
175: }
176: }
177:}

Aqui utilizamos una barrera (en el caso de Java 1.5.0, un Latch), la cual nos permite notificarle a las demás hebras en Java que ya terminamos de ‘tratar’ de bajar la imagen. Ahora el código de la clase que se encarga de obtener la información de las imagenes y de coordinar la ejecución de las hebras:

   1:package com.blogspot.elangelnegro.flickr;
2:
3:import java.util.ResourceBundle;
4:import java.util.Properties;
5:import java.util.Vector;
6:import java.util.Hashtable;
7:
8:import javax.xml.parsers.DocumentBuilder;
9:import javax.xml.parsers.DocumentBuilderFactory;
10:import javax.xml.parsers.FactoryConfigurationError;
11:import javax.xml.parsers.ParserConfigurationException;
12:
13:import org.xml.sax.SAXException;
14:import org.xml.sax.SAXParseException;
15:
16:import org.w3c.dom.Document;
17:import org.w3c.dom.DOMException;
18:import org.w3c.dom.NodeList;
19:import org.w3c.dom.Node;
20:
21:import java.net.URL;
22:
23:import java.io.IOException;
24:import java.io.ByteArrayInputStream;
25:import java.io.File;
26:import java.io.FileOutputStream;
27:import java.io.BufferedOutputStream;
28:import java.io.InputStream;
29:
30:import org.apache.xmlrpc.XmlRpcClient;
31:import org.apache.xmlrpc.XmlRpcException;
32:import org.apache.xmlrpc.XmlRpc;
33:
34:import java.util.concurrent.CountDownLatch;
35:import java.util.concurrent.BrokenBarrierException;
36:import java.util.concurrent.TimeUnit;
37:
38:/**
39: * I wrote this application because Flickr doesn't have an automatic way to do
40: * backups of the original images. For that reason I decided to make a quick
41: * backup utility that uses the Flickr API to get the list of photos
42: *
43: * For more information, please check the following URLS:
44: * <ul>
45: * <li> <a href="http://www.flickr.com/services/api/misc.urls.html">Flickr URL format</a>
46: * <li> <a href="http://www.flickr.com/services/api/">Description of the API</a>
47: * <li> <a href="http://www.flickr.com/forums/help/5304/">The problem I had to solve :)</a>
48: * <li> <a href="http://ws.apache.org/xmlrpc/client.html">Apache XML-RPC</a>
49: * <li> <a href="http://prdownloads.sourceforge.net/elangelnegro/DownloadPublicPictures.plx?download">Original Perl version I wrote</a>
50: * </ul>
51: * License: GPL
52: * Blog: http://elangelnegro.blogspot.com
53: * @author Jose Vicente Nunez Zuleta
54: * @version 0.1 - 04/01/2005
55: */
56:public final class FlickrBackup {
57:
58: private static final ResourceBundle BUNDLE =
59: ResourceBundle.getBundle(FlickrBackup.class.getName());
60:
61: private static final Properties CONFIG = System.getProperties();
62:
63: /**
64: * Maximun number of pictures per page
65: */
66: public static final int MAX_PICTURES_PER_PAGE = 15;
67:
68: /**
69: * Hide the constructor
70: *
71: */
72: private FlickrBackup() {
73: // Empty
74: }
75:
76: /**
77: * Command line invocation
78: * @param args Command line args
79: * <ul>
80: * <li> args[0] Is the Flickr user
81: * <li> args[1] is the password for that account
82: * <li> args[2] is the directory where we will save all the photos
83: * </ul>
84: * @throws Exception if there is any error while downloading the pictures
85: */
86: public static void main(String [] args) throws Exception {
87: if (! ( (args != null) && (args.length == 3) )) {
88: throw new IllegalArgumentException(
89: BUNDLE.getString("com.blogspot.elangelnegro.flickr.FlickrBackup.main.messages.allparam")
90: );
91: }
92: File dir = new File(args[2]);
93: if ( dir.exists() && ! dir.isDirectory() ) {
94: throw new IllegalArgumentException(
95: BUNDLE.getString("com.blogspot.elangelnegro.flickr.FlickrBackup.main.messages.dirNotFound")
96: );
97: }
98: Vector params = new Vector();
99: Hashtable struct = new Hashtable();
100: DocumentBuilderFactory factory =
101: DocumentBuilderFactory.newInstance();
102: DocumentBuilder builder =
103: factory.newDocumentBuilder();
104: Document document = null;
105: XmlRpcClient xmlrpc = null;
106: try {
107: xmlrpc = new XmlRpcClient(
108: BUNDLE.getString("com.blogspot.elangelnegro.flickr.FlickrBackup.xmlrpc.url")
109: );
110:
111: /* Get the Flickr numeric user ID
112: * http://www.flickr.com/services/api/flickr.people.findByUsername.html
113: */
114: struct.put(
115: "api_key",
116: BUNDLE.getString("com.blogspot.elangelnegro.flickr.FlickrBackup.xmlrpc.key")
117: );
118: struct.put(
119: "find_email",
120: args[0]
121: );
122: params.add(struct);
123: String result =
124: (String) xmlrpc.execute (
125: "flickr.people.findByEmail",
126: params
127: );
128: document = builder.parse(
129: new ByteArrayInputStream(
130: result.getBytes()));
131: NodeList nodes = document.getElementsByTagName("user");
132: String userId =
133: nodes.item(0).getAttributes().getNamedItem("nsid").getNodeValue();
134: System.out.println(
135: BUNDLE.getString("com.blogspot.elangelnegro.flickr.FlickrBackup.main.messages.connecting") +
136: ": " +
137: args[0]
138: );
139: struct.clear();
140: params.clear();
141: int currentPage = 1;
142: int totalCount = 0;
143: int totalPages = 1;
144: String maxPages = String.valueOf(MAX_PICTURES_PER_PAGE);
145: /*
146: * Get the total number of photos and pages.
147: * This will control how many photos are downloaded
148: * at the same time.
149: * The only way to know that is to search
150: */
151: struct.put(
152: "api_key",
153: BUNDLE.getString("com.blogspot.elangelnegro.flickr.FlickrBackup.xmlrpc.key")
154: );
155: struct.put("user_id", userId);
156: struct.put("per_page", maxPages);
157: struct.put("page", String.valueOf(currentPage));
158: params.add(struct);
159: result = (String) xmlrpc.execute (
160: "flickr.photos.search",
161: params
162: );
163: struct.clear();
164: params.clear();
165: /*
166: * Get the photo and page count.
167: */
168: document = builder.parse(
169: new ByteArrayInputStream(
170: result.getBytes()));
171: nodes = document.getElementsByTagName("photos");
172: totalCount =
173: Integer.parseInt(nodes.item(0).getAttributes().getNamedItem("total").getNodeValue());
174: totalPages =
175: Integer.parseInt(nodes.item(0).getAttributes().getNamedItem("pages").getNodeValue());
176: System.out.println(
177: BUNDLE.getString("com.blogspot.elangelnegro.flickr.FlickrBackup.main.messages.found") +
178: ": " +
179: totalCount +
180: ", " +
181: totalPages
182: );
183: for (currentPage = 1; currentPage <= totalPages; currentPage++) {
184: /*
185: * Get now the photo list. Flickr has a limit of
186: * how many photos can show on a list (500) so get
187: * the first page, find how many photos are and then iterate
188: * all the pages. This number is an upper limit.
189: * For that we have to repeat the search on a loop.
190: */
191: struct.put(
192: "api_key",
193: BUNDLE.getString("com.blogspot.elangelnegro.flickr.FlickrBackup.xmlrpc.key")
194: );
195: struct.put("user_id", userId);
196: struct.put("per_page", maxPages);
197: struct.put("page", String.valueOf(currentPage));
198: params.add(struct);
199: result =
200: (String) xmlrpc.execute (
201: "flickr.photos.search",
202: params
203: );
204: struct.clear();
205: params.clear();
206: /*
207: * Get the photo list. Using this information, construct the phot URL as indicated here:
208: * http://www.flickr.com/services/api/misc.urls.html
209: */
210: ByteArrayInputStream xmlStream = new ByteArrayInputStream(result.getBytes());
211: document = builder.parse(xmlStream);
212: xmlStream.close();
213: nodes = document.getElementsByTagName("photo");
214: System.out.println(
215: "\t" +
216: BUNDLE.getString("com.blogspot.elangelnegro.flickr.FlickrBackup.main.messages.currPage") +
217: ": " +
218: currentPage +
219: ": " +
220: nodes.getLength()
221: );
222: // Prepare the latch
223: CountDownLatch startSignal = new CountDownLatch(1);
224: CountDownLatch doneSignal = new CountDownLatch(nodes.getLength());
225: for (int i = 0; i < nodes.getLength(); i++) {
226: String server = (String) nodes.item(i).getAttributes().getNamedItem("server").getNodeValue();
227: String secret = (String) nodes.item(i).getAttributes().getNamedItem("secret").getNodeValue();
228: String title = (String) nodes.item(i).getAttributes().getNamedItem("title").getNodeValue();
229: String id = (String) nodes.item(i).getAttributes().getNamedItem("id").getNodeValue();
230: // Download the picture
231: Thread thread = new Thread(
232: new Download(server, id, secret, title, dir, startSignal, doneSignal),
233: server + "-" + id + "-" + secret
234: );
235: thread.start();
236: } // End for
237: // Start all the threads
238: startSignal.countDown();
239: // We could do something else here, but we wont :)
240:
241: // Wait for all the other threads to finish
242: doneSignal.await(60*5, TimeUnit.SECONDS);
243: } // End launching threads - while
244: } catch (XmlRpcException xmlexp) {
245: throw xmlexp;
246: } catch (SAXException sxe) {
247: Exception exp = sxe;
248: if (sxe.getException() != null) {
249: exp = sxe.getException();
250: }
251: exp.printStackTrace();
252: } catch (IOException ioexp) {
253: throw ioexp;
254: } finally {
255: struct.clear();
256: params.clear();
257: }
258: }
259:}

Después de unos cambios, este es el tiempo de ejecución:


Connecting as: disposablehero3000-flickr3@yahoo.com
Pictures found: 53

real 0m32.087s
user 0m1.969s
sys 0m0.788s
[josevnz@localhost FlickrBackup]$

!Mucho más rápida!

La aplicación aún no está completa, sólo me concentré esta vez en mejorar su ejecución:

  • La versión final guarda tambien la metadata de cada foto además de la imagen, por lo que en teoría deberia ser fácil escribir una aplicación que pueda restaurar las imagenes de vuelta en Flickr.
  • La aplicación reportará si pudo bajar o no cada una de las imagenes.
  • La aplicación utilizará una interfaz gráfica (Swing?) lo cual hará más fácil su uso.
  • Averiguar correctamente la extensión de la imagen original. Ahorita asumo que todas son JPG lo cual es falso (el API de Flickr no soporta eso actualmente. Aunque hay una forma de saber.).

Les dejo un par de tutoriales sobre Threads (1, 2), y por supuesto con el enlace a el código. Pienso acomodarlo ahora para usar Swing de manera que sea más amigable para que pueda hacer sus respaldos.