downloadsubtitle: script para baixar legendas de filmes automaticamente no shell do GNU/Linux

Me acostumei a usar o legendas.tv para baixar legendas de filmes e acabei nunca me perguntando se haveria um jeito mais fácil de baixá-las. Hoje fui assistir um filme e, ao entrar no navegador para baixar sua legenda, me deparei com a mensagem de que o site estava fora do ar:

Mensagem do legendas.tv fora do ar.

A situação me obrigou a procurar outros sites e outras formas de baixar legendas. Minha primeira ideia foi usar o opensubtitles.org, que já havia usado algumas outras vezes. Chegando lá e procurando pelo filme que eu desejava, vi muitas opções e não estava muito claro que legenda baixar para a versão do filme que eu tinha.

Então resolvi dar uma fuçada na pesquisa avançada do site, onde acabei encontrando uma pesquisa por hash. Hash, em computação, é uma função que “resume” uma informação gigante (tipo um arquivo bem grande) numa informação bem pequena (tipo 16 caracteres) que o represente de forma única (ou quase única). A pesquisa por hash, no caso desse site, consiste em procurar uma legenda utilizando esse “ID” do arquivo (ou seja, não importa seu nome).

Achei a possibilidade tão legal que resolvi fazer um programa para nunca mais precisar abrir o navegador quando eu quiser baixar a legenda de um filme. Escrevi um minúsculo programa em C chamado oshash (de OpenSubtitles Hash) para calcular o hash de um filme de acordo com a especificação do site (que não requer nada, a não ser um compilador de C e a biblioteca padrão) e um script (bem tosco, mas funcional) chamado downloadsubtitle que usa o programa oshash (e pequenos programas que todo mundo tem, tipo grep, sed, wget e unzip) para baixar a legenda.

O funcionamento ficou bem fácil: para baixar uma legenda em qualquer língua, basta você digitar downloadsubtitle arquivo.avi para baixar a legenda do “arquivo.avi” (que já vai ser automaticamente nomeada como “arquivo.srt”). Se você quiser especificar uma língua (por exemplo, português do Brasil), é só digitar downloadsubtitle arquivo.avi pob (pob é o código do português do Brasil). Se você quiser baixar uma legenda em inglês ou espanhol, pode usar downloadsubtitle arquivo.avi eng,esp.

Exemplo de funcionamento

$ ls
Amelie [Amélie Poulain].2001.BRRip.x264.AAC[5.1]-VLiS.mkv
$ downloadsubtitle Amelie\ \[Amélie\ Poulain\].2001.BRRip.x264.AAC\[5.1\]-VLiS.mkv pob
Requested language: pob
Movie hash: bcdc90cf4873c09b
Subtitle ID: 4642726
Subtitle: Amelie [Amélie Poulain].2001.BRRip.x264.AAC[5.1]-VLiS.srt
$ ls
Amelie [Amélie Poulain].2001.BRRip.x264.AAC[5.1]-VLiS.mkv  Amelie [Amélie Poulain].2001.BRRip.x264.AAC[5.1]-VLiS.srt
$

E aí o filme está pronto para você assistir com o mplayer ou com o seu programa favorito.

Código

Este é o código inicial. Está aqui para fins históricos. Não será atualizado. Use a próxima seção (Download) para baixar a última versão, com bugs corrigidos, tratamento de erros e possivelmente novas funcionalidades.

#include <stdio.h>
#include <stdlib.h>

void usage(char *name) {
    printf("Usage: %s <file>\n", name);
    exit(1);
}

int main(int argc, char *argv[]) {
    unsigned long long buf[16384], c = 0;
    FILE *in;
    int i;
    if (argc != 2) {
        usage(argv[0]);
    }
    in = fopen(argv[1], "rb");
    if (in == NULL) {
        usage(argv[0]);
    }
    fread(buf, 8192, 8, in);
    fseek(in, -65536, SEEK_END);
    fread(&buf[8192], 8192, 8, in);
    for (i = 0; i < 16384; i++) {
        c+= buf[i];
    }
    c+= ftell(in);
    fclose(in);
    printf("%016llx\n", c);
    return 0;
}
#!/bin/bash

usage() {
    echo "Usage: $0 <file> [lang]"
    echo "Examples:"
    echo "$ $0 movie.avi pob         # brazilian portuguese"
    echo "$ $0 movie.avi por,pob     # any portuguese"
    echo "$ $0 movie.avi eng         # english"
    echo "$ $0 movie.avi all         # any language"
    exit
}

if [ $# -lt 1 ]; then
    usage
elif [ $# -gt 2 ]; then
    usage
fi

if [ $# = 2 ]; then
    lang=$2
else
    lang="any"
fi

echo "Requested language: $lang"
output=$(echo "$1" | sed 's/\.[^.]*$/.srt/')
oshash=$(oshash "$1")
echo "Movie hash: $oshash"
subid=$(wget "http://www.opensubtitles.org/en/search/sublanguageid-$lang/moviehash-$oshash/rss_2_00" -q -O - \
    | grep '<link />.*en/subtitles' | sed 's|.*en/subtitles/||; s|/.*||' | head -n1)
echo "Subtitle ID: $subid"
wget "http://www.opensubtitles.org/en/subtitleserve/sub/$subid" -q -O - | gunzip > "$output" 2> /dev/null
echo "Subtitle: $output"

Download

Criei um repositório no Github para colocar o código: github.com/tmadeira/downloadsubtitle

Para quem tem git, é possível baixar com git clone https://github.com/tmadeira/downloadsubtitle.git

Para quem não tem, dá pra baixar em ZIP daqui: github.com/tmadeira/downloadsubtitle/archive/master.zip

O programa ainda não está empacotado bonitinho (não tem nem Makefile ou instruções de instalação). Se futuramente vier a ter, este post será atualizado. Em resumo, basta compilar o código em C (digitando gcc oshash.c -o oshash) e colocar os arquivos oshash e downloadsubtitle numa pasta do seu $PATH (por exemplo, /usr/local/bin).

Sugestões e correções são bem-vindas.

Gerando tabelas de hash MD5

A idéia veio do nada. Na verdade, eu estava fazendo um freelance e procurando uma função pra “ordenar” um vetor aleatoriamente no PHP (no fim usei usort, que recebe uma função de comparação tipo o qsort do C) e me deparei com um comentário lá no PHP.net de um cara que usou geração de números aleatórios para criar um algoritmo de criptografia pra usar no lugar de MD5 e SHA, porque ele não confiava mais no MD5 e no SHA porque segundo ele existem tabelas na internet, o que torna um sistema muito vulnerável (para quem tem acesso ao banco de dados).

Procurei na internet e na verdade eu não encontrei muitos bancos de dados com vários MD5 não, ao menos não visíveis no Google quando se procura por um hash. Só pra strings como “1234”, mas não achei nem pra “060790” (minha data de nascimento). Veja você mesmo: 81811b48cc07432fc550cb42d4ab3e8f

Então pensei: são 31 dias por mês, 12 meses por ano, 100 anos considerando que os anos são representados por dois dígitos. Isso me dá 31 x 12 x 100 = apenas 37200 hashes. O custo pra gerar isso deve ser minúsculo, por que ninguém faz?

A idéia de fazer com essas datas de nascimento veio do fato de vários usuários leigos que conheço utilizarem data de nascimento pra suas senhas (muito também pelo fato de eles usarem essa mesma senha nas senhas de seis dígitos numéricos do banco e esse tipo de coisa). Então lá fui eu pro código. A princípio escrevi em cerca de 30 segundos – 1 minuto o seguinte código em Ruby:

require "md5"

100.times do |ano|
  12.times do |j|
    mes = j+1

    31.times do |i|
      dia = i+1
      string = sprintf("%02d%02d%02d", dia, mes, ano)

      md5 = MD5.new(string).to_s
      puts "#{string}: #{md5}"
    end
  end
end

É funcional e até eficiente…

tiago@flick ~ $ time ruby genmd5.rb > hashes-ruby

real    0m1.662s
user    0m0.620s
sys     0m0.028s

Mas minha geekialidade não permitiu que eu parasse por aqui. Nesse momento eu já nem me lembrava do freelance que estava fazendo e resolvi ver como usar MD5 em C. Meu primeiro chute foi um man md5 no terminal, que já me retornou a resposta da vida, do universo e tudo mais: o cabeçalho openssl/md5.h e sua função MD5:

unsigned char *MD5(const unsigned char *d, unsigned long n,
                 unsigned char *md);

Então lá fui eu pro programa:

#include <stdio.h>
#include <openssl/md5.h>

int main() {
  int dia, mes, ano;
  unsigned char string[STRING_LENGTH], md[MD5_DIGEST_LENGTH];
  for (ano = 0; ano < 100; ano++) {
    for (mes = 0; mes < 12; mes++) {
      for (dia = 0; dia < 31; dia++) {
        sprintf(string, "%02d%02d%02d", dia+1, mes+1, ano);
        printf("%sn", MD5(string, sizeof(string), md));
      }
    }
  }
  return 0;
}

Ao rodar, recebi saídas com caracteres estranhos e nenhum resultado visível. Corri pro Google. Procurei, procurei, e NINGUÉM usa essa maldita função MD5 do C num programa simples e não há exemplos nem howto de como utilizá-la. Depois de uns 30 minutos quebrando a cabeça (pra mais), percebi que alguns códigos que usavam isso na internet usavam %x (é o código pro printf imprimir inteiros hexadecimais) para imprimir caracteres do MD5 na tela. Aí encontrei algo assim:

printf("%x%x...%x%x", md[0], md[1], ..., md[14], md[15]);

(e pior que estou falando sério… tem gente na internet que não conhece for!)

E caiu a ficha. O MD5 tem 16 inteiros hexadecimais de um byte, 32 caracteres. Escrever o código abaixo me tomou bastante tempo, mas uma aprendizagem interessante e um ânimo pra voltar pro meu freelance:

#include <stdio.h>
#include <openssl/md5.h>

#define STRING_LENGTH 6

int main() {
  int dia, ano, mes;
  int i;
  unsigned char md[MD5_DIGEST_LENGTH], string[STRING_LENGTH];

  for (ano = 0; ano < 100; ano++) {
    for (mes = 0; mes < 12; mes++) {
      for (dia = 0; dia < 31; dia++) {
        sprintf(string, "%02d%02d%02d", dia+1, mes+1, ano);
        MD5(string, sizeof(string), md);
        printf("%s: ", string);
        for (i = 0; i < 16; i++) {
          printf("%02x", md[i]); /* Note isso aqui! */
        }
        printf("n");
      }
    }
  }
  return 0;
}

O “Note isso aqui!” ainda foi uma sacanagem incrível, o código que eu disse do cara que imprimia %x%x%x… não funciona na prática, porque quando um dos dígitos é 0X, ele só imprime X.

Enfim compilei meu código com -lssl (importante para C-zeiros de primeira viagem) e voi lá:

tiago@flick ~ $ time ./md5 > hashes-c

real    0m0.397s
user    0m0.136s
sys     0m0.028s

Confesso que é coisa de nerd mesmo querer otimizar um código que só serve pra gerar um arquivo de hashes e já o fez, mas no momento em que terminei esse teste, pensei: preciso postar isso no blog pra documentar o uso da função MD5 no C, antes que mais alguém perca o tempo que eu perdi.

Pra quem se pergunta se eu realmente fiz tudo certo:

tiago@flick ~ $ diff senhas-c senhas-ruby
tiago@flick ~ $

(i.e. ou eu errei nos dois ou eu não errei em nenhum)

Útil, não? Não. Na verdade isso não serve pra absolutamente nada, a não ser que você roube o banco de dados de alguém e esse alguém usa como senha a data de nascimento da sua mãe. Mas aí ele merece mesmo que você pegue sua senha, então isso não muda nada.

Dá pra adaptar o código pra gerar outras tabelas, mas por favor não use isso pra nada maligno, e falo sério. Meus fins foram absolutamente educacionais/acadêmicos (eu queria aprender a trabalhar com a função MD5 da biblioteca OpenSSL, não dominar o mundo) e estou postando aqui para ajudar os que também vão querer usar essa função.

Comentários de crackers serão ignorados.

© 2005–2020 Tiago Madeira