miércoles, 9 de noviembre de 2011

Mi sistema personal para organizar series de televisión (II)

Mi sistema para descargar y consumir series ha cambiado sensiblemente desde que escribí este post

Ahora tengo un NAS Buffalo en mi red con cliente de Bittorrent embebido y eso lo cambia todo. Los capítulos ya no bajan a un directorio de mi equipo, sino al disco en red. Antes utilizaba el propio Transmission para mover los capítulos descargados a sus carpetas correspondientes. Una por serie y temporada. Con el uTorrent embebido en el NAS eso ya no es posible. Y el friki que hay en mi no duerme agusto por las noches si no está todo perfectamente automatizado.

Me he hecho un pequeño script que soluciona este problema y lo tengo programado en el cron para lanzarse cada hora. El demonio hace lo siguiente:
  1. Busca en el directorio de descargas archivos comprimidos y los extrae
  2. Elimina los residuos: *.rar, *.r01, *.r02...
  3. Renombra los capítulos para establecer correctamente: serie-episodio-nombre_del_captítulo
  4. Mueve cada capítulo a su directorio correspondiente.
Los pasos 3 y 4 son realmente los interesantes y para ellos hago uso de la herramienta tvnamer, que obtiene la información de las series de thetvdb.com y las renombra convenientemente. 

Aquí tenéis el script para que cualquiera pueda adaptarlo a sus necesidades

#!/bin/bash

DOWNLOADS='/Volumes/share/downloads/bittorrent/'
TV_SHOWS='/Volumes/share/video/tvshows/'

    # Unrar and clean
    cd $DOWNLOADS
    echo "Unraring from $DOWNLOADS"
    find . -type f -name '*.rar' -exec echo "Unraring: " {} \; -exec rar x -y -o- {} \;
    if [ $? == 0 ]
    then
        find . -type f -name '*.r??' -exec echo "Deleting: " {} \; -exec unlink {} \;
    fi
   
    # Rename and organize
    echo "Renaming and moving to $TV_SHOWS"
    find . -type f \( -name *.mkv  -o -name '*.avi' \) -exec tvnamer --batch -r -m -d "$TV_SHOWS%(sersname)s/%(seasonnumber)d/" {} \; >> magic.log 2>&1

Los modificadores al comando tvnamer significan:
  • --batch: hacer todo sin preguntar al usuario
  • -r: recursivo
  • -m: mover a
  • -d: directorio al que se van a mover los archivos una vez renombrados. Se incluirá el nombre de la serie y el número de temporada como subdirectorios
Happy automatization!

sábado, 9 de julio de 2011

Autoimportar libros a Calibre

Acabo de comprarme un Kindle. Calibre está genial para alimentarlo pero después de una mañana el proceso de descargar-importar a calibre-copiar a kindle se hace tedioso.

Me he creado un Workflow con Automator para importar directamente a Calibre todos los *.pub, *.pdf y *.mobi que aparezcan en el directorio ./Downloads de mi equipo. Después los envía directamente a la papelera para dejar el equipo limpio de basura.

El único requisito es tener añadido el comando 'calibredb' al PATH del usuario. Esto se puede hacer por ejemplo creando un enlace simbólico en /usr/bin

> sudo ln -s /Applications/calibre.app/Contents/console.app/Contents/MacOS/calibredb /usr/bin/calibredb

Al que le interese puede descargarlo y modificarlo a su gusto aquí

jueves, 7 de julio de 2011

Parsear JSON utilizando Gson

Voy a explicar cómo parsear objetos JSON utilizando la librería open source Gson (http://code.google.com/p/google-gson/) utilizando como ejemplo los trending topics de Twitter.
La ventaja de utilizar Gson es que la generación de objetos Java es inmediata, no más toString() Basta definir el modelo que representará la respuesta JSON, pasárselo al parseador de Gson y él se encargará de hacer todo el trabajo. Esto también funciona en sentido inverso: serializar objetos nunca fue tan fácil.
La consulta de los trending topics mundiales de Twitter tiene esta pinta:
[
    {
        "created_at":"2011-07-06T15:10:14Z",
        "as_of":"2011-07-06T15:22:20Z",
        "locations":[
            {
                "name":"Worldwide",
                "woeid":1
            }
        ],
        "trends":[
            {
                "url":"http:\/\/search.twitter.com\/search?q=%23nationalkissingday",
                "events":null,
                "name":"#nationalkissingday",
                "promoted_content":null,
                "query":"%23nationalkissingday"
            },
            {
                "url":"http:\/\/search.twitter.com\/search?q=%23goseeksomehelp",
                "events":null,
                "name":"#goseeksomehelp",
                "promoted_content":null,
                "query":"%23goseeksomehelp"
            },
            {
                "url":"http:\/\/search.twitter.com\/search?q=%23elmejorinvento",
                "events":null,
                "name":"#elmejorinvento",
                "promoted_content":null,
                "query":"%23elmejorinvento"
            },
            {
                "url":"http:\/\/search.twitter.com\/search?q=BTWBall",
                "events":null,
                "name":"BTWBall",
                "promoted_content":null,
                "query":"BTWBall"
            },
            {
                "url":"http:\/\/search.twitter.com\/search?q=%22Brasil%203x2%20Cuba%22",
                "events":null,
                "name":"Brasil 3x2 Cuba",
                "promoted_content":null,
                "query":"%22Brasil%203x2%20Cuba%22"
            },
            {
                "url":"http:\/\/search.twitter.com\/search?q=JoeJonasIsPERFECT",
                "events":null,
                "name":"JoeJonasIsPERFECT",
                "promoted_content":null,
                "query":"JoeJonasIsPERFECT"
            },
            {
                "url":"http:\/\/search.twitter.com\/search?q=%22Abbey%20Dawn%22",
                "events":null,
                "name":"Abbey Dawn",
                "promoted_content":null,
                "query":"%22Abbey%20Dawn%22"
            },
            {
                "url":"http:\/\/search.twitter.com\/search?q=%22Charlie%20Adam%22",
                "events":null,
                "name":"Charlie Adam",
                "promoted_content":null,
                "query":"%22Charlie%20Adam%22"
            },
            {
                "url":"http:\/\/search.twitter.com\/search?q=%22Noel%20Gallagher%22",
                "events":null,
                "name":"Noel Gallagher",
                "promoted_content":null,
                "query":"%22Noel%20Gallagher%22"
            },
            {
                "url":"http:\/\/search.twitter.com\/search?q=Boonen",
                "events":null,
                "name":"Boonen",
                "promoted_content":null,
                "query":"Boonen"
            }
        ]
    }
]
Como se puede ver lo que estamos recibiendo es: - Array de un solo elemento con: - Atributos informando de la fecha de generación. - Array de localizaciones con un solo elemento. (no lo utilizaremos) - Array de 'trends' con 10 elementos.
A su vez, cada trend tiene varios atributos. Para este ejemplo nos quedaremos solo con URL y Name.
Lo siguiente es crear el modelo que representará esta estructura de datos. Para ello se necesitan dos clases muy simples. La primera es TwitterTrends que modela un elemento tipo 'trend'
package es.lgvalle.trendsparser;
import com.google.gson.annotations.SerializedName;

public class TwitterTrend {
 @SerializedName("name")
 private String name;

 @SerializedName("url")
 private String url;

 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }

 public String getUrl() {
  return url;
 }

 public void setUrl(String url) {
  this.url = url;
 }
}
La segunda es TwitterTrends que contiene una lista de TwitterTrend junto con la fecha de creación
package es.lgvalle.trendsparser;
import java.util.List;
import com.google.gson.annotations.SerializedName;

public class TwitterTrends {

 @SerializedName("as_of")
    private String as_of;
 
 @SerializedName("created_at")
    private String created_at;
 
 @SerializedName("trends")
    private List trends;
 
    public String getCreated_at() {
  return created_at;
 }
 public void setCreated_at(String created_at) {
  this.created_at = created_at;
 }
 public String getAs_of() {
        return as_of;
    }
    public void setAs_of(String asOf) {
        as_of = asOf;
    }
    public List getTrends() {
        return trends;
    }
    public void setTrends(List trends) {
        this.trends = trends;
    }
}
En este ejemplo voy a generar un objeto TwitterTrends que mostrará sus 'trends' en un FlipperView. Al hacer click sobre un elemento navegaremos a su url asociada.
El código que hace esto quedaría así:
Type objType = new TypeToken>() {}.getType();
ArrayList trendsList = (ArrayList) JSONClient.getJson(TRENDS_URL, objType);

// El api de Twitter devuelve un array con un único elemento.
TwitterTrends objs = (TwitterTrends) trendsList.get(0);

for (final TwitterTrend tr : objs.getTrends()) {
 TextView trend = (TextView) getLayoutInflater().inflate(R.layout.trend, null);
 trend.setText(tr.getName());
 trend.setOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
   startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(tr.getUrl())));
   }
 });
 mFlipper.addView(trend);
}
mFlipper.startFlipping();
Como se puede ver en primer lugar hay que crear un TypeToken especificando el tipo de objeto que modela el JSON que vamos a recibir.
Este TypeToken es propio de la librería Gson y sirve para lidiar con la pérdida de tipo al crear objetos genéricos en Java. Básicamente el problema es que en Java, cuando creamos un List y pedimos el tipo obtenemos 'List', la información sobre String se pierde. Para más información: http://download.oracle.com/javase/tutorial/java/generics/erasure.html
El Type Erasure es útil para muchas cosas pero un gran problema para nuestro ejemplo. A Gson necesitamos especificarle exactamente el objeto que vamos a construir y ahí es donde entra TypeToken.
Finalmente la clase JSONClient.java quedaría así:
package es.lgvalle.trendsparser;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Type;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

import com.google.gson.Gson;

public class JSONClient {
 public static Object getJson(String url, Type objType) {
  Gson gson = new Gson();
  Reader r = new InputStreamReader(getStream(url));
  Object fromJson = gson.fromJson(r, objType);
  return fromJson;
 }

 private static InputStream getStream(String url) {
  HttpGet request = new HttpGet(url);
  InputStream is = null;
  try {
   HttpResponse response = new DefaultHttpClient().execute(request);
   final int statusCode = response.getStatusLine().getStatusCode();
   if (statusCode != HttpStatus.SC_OK) {
    Log.v("Error " + statusCode + " for URL " + url);
   } else {
    is = response.getEntity().getContent();
   }
   
  } catch (IOException e) {
   request.abort();
   Log.v("Error for URL " + url);
  }
  Log.v("Got stream from "+url);
  return is;
 }
}
Done.
El código completo de este ejemplo está disponible aquí.

jueves, 9 de junio de 2011

Custom scrollview con scroll lock dinámico

Con esta sencilla clase podemos tener un scrollview que deshabilite el scroll dinámicamente en función de un flag

public class MenuScrollView extends ScrollView {


/**

* Flag para controlar el bloqueo del scroll

*/

boolean lock = false;


public boolean isLocked() {

return lock;

}


public void setLock(boolean lock) {

this.lock = lock;

}


public MenuScrollView(Context context, AttributeSet attrs, int i) {

super(context, attrs, i);

this.setVerticalScrollBarEnabled(false);

this.setHorizontalScrollBarEnabled(false);

}


public MenuScrollView(Context context, AttributeSet attrs) {

super(context, attrs);

this.setVerticalScrollBarEnabled(false);

this.setHorizontalScrollBarEnabled(false);

}


public MenuScrollView(Context context) {

super(context);

this.setVerticalScrollBarEnabled(false);

this.setHorizontalScrollBarEnabled(false);

}


/**

* Return true -> no scroll

*/

public boolean onTouchEvent(MotionEvent ev) {

if (lock) {

return true;

} else {

return super.onTouchEvent(ev);

}


}


/**

* Return false -> no scroll

*/

public boolean onInterceptTouchEvent(MotionEvent ev) {

if (lock) {

return false;

} else {

return super.onInterceptTouchEvent(ev);

}

}

}



miércoles, 8 de junio de 2011

Añadir contacto

Añadir contacto al terminal utilizando la aplicación nativa de agenda:

Intent i=new Intent(Intent.ACTION_INSERT_OR_EDIT);
i.setType(Contacts.CONTENT_ITEM_TYPE);
i.addCategory(Intent.CATEGORY_DEFAULT);

// Nombre del contacto
i.putExtra(Insert.NAME"Nombre");
// Teléfono principal. En algunos terminales si no se especifica teléfono no aparece el contacto (HTC Desire)
i.putExtra(Insert.PHONE"954 123 456");
i.putExtra(Insert.PHONE_TYPE, Phone.TYPE_WORK);
// Teléfono secundario.
i.putExtra(Insert.SECONDARY_PHONE"954 789 012");
i.putExtra(Insert.SECONDARY_PHONE_TYPE, Phone.TYPE_OTHER);
// Fax
i.putExtra(Insert.TERTIARY_PHONE"912 345 567");
i.putExtra(Insert.TERTIARY_PHONE_TYPE, Phone.TYPE_FAX_WORK);
// Email
i.putExtra(Insert.EMAIL, "luis@mail.com");
// Dirección
i.putExtra(Insert.POSTAL, "Calle Sierpes 1, 41002, Sevilla");
startActivity(i);

miércoles, 16 de marzo de 2011

Yo, versión 3.0

Hoy he cumplido 30 años. Esta es una lista con las 10 cosas más importantes que he aprendido hasta ahora.

  1. Nadie da duros a cuatro pesetas. Esta regla condiciona todas las demás.
  2. La mayoría de los políticos mienten y roban en beneficio propio. Si tienes que votar, vota al que pienses que lo hará menos.
  3. El principal y único objetivo de una empresa es ganar dinero. Sus acciones y declaraciones están siempre motivadas por eso.
  4. Casi todo lo que nos rodea son empresas que se rigen por los puntos 1 y 3: televisiones, periódicos, equipos de fútbol, grupos musicales, ... Piensa en ello la próxima vez que te declares fan de una empresa. Ellos no son fan de ti, sólo de lo que puedan sacar de ti.
  5. Busca algo que harías gratis y conviértelo en tu profesión. Es la mejor forma de ser feliz durante todo el día.
  6. Nunca esperes demasiado para cambiar algo que no te guste: piso, ciudad, trabajo, pareja... Si estás pensándotelo es que ya es el momento. Siempre es más fácil de lo que imaginas.
  7. ...pero nunca cambies de trabajo sólo porque el salario sea mayor. Ten presente la regla 1 y piensa por qué es mayor.
  8. Enfadarse por los problemas no sirve de nada. ¿Puedes solucionarlo? Hazlo. ¿No puedes pero conoces a quien puede? Búscalo y que lo solucione. ¿No hay solución posible? Entonces para qué enfadarse.
  9. Aprende a escuchar a la gente. Pensar en tus cosas mientras otro habla esperando la oportunidad de soltar lo tuyo no es escuchar.
  10. Tus amigos te quieren a pesar de todo

martes, 15 de febrero de 2011

Android: write EASY on internal storage

Recently I found that not all sdcards filesystem root paths starts with /sdcard. So I was getting an Exception trying to write files in a location that doesn't exist. Epic fail in my code.

Filesystem root path independent code

String content = "Hola mundo";       
FileOutputStream fos = openFileOutput(FILE_NAME, Context.MODE_PRIVATE);
fos.write(content.getBytes());
fos.close();

This will create a file on your application private space, so no other applications can access to it.

Great thing is that you don't need to know anything about paths, permissions, inputstreams, ... Just let Android handle it for you.