How to convert ebooks in bulk with Python and Calibre






Prior knowledge


It's not a must, but it helps a lot to have basic knowledge of the following Python's built-in modules:

  • os
  • subprocess
  • shutil
  • logging
  • time


The most important is the fire within for learning and practicing code.


Introduction to Calibre


Calibre is an open-source ebook management software written in Python. Other than organizing features, it offers the functionality to convert a document from one format to another.

The default installation is shipped with a command-line tool, ebook-convert, which can be used for ebook conversion directly from the command prompt, or terminal in UNIX-based distributions. 


Introduction to ebook-convert


ebook-convert isn't rich in options. It takes as input the ebook and as output the name with the desired format as an extension.

The following illustrates how it can be used.

ebook-convert input_file output_file


It's also possible to just specify the type of format for the ebook in the output, and the name of the file is derived from the name of the input document.


ebook-convert input_file .pdf

Note: The extension in the output file is important, otherwise ebook-convert treats it as a directory, where HTML scripts are written.

Unfortunately, ebook-convert deals with only one document at a time.


Python offers a solution



Shipped with many built-in modules that deal with the underlying functionalities of the operating system, Python is the perfect language for scripting. 

The subprocess module helps to open child processes and deal with them accordingly. With the utilities being offered by it, a programmer can interact with the input and output data of a spawned process.


The ability to obtain the return code of a subprocess is useful when it is required to check the status of a program. 


Launch a child process with subprocess


Various ways exist to launch a process with the help of the Python's module subprocess. For the purpose of this tutorial, the Popen class will be illustrated in action.

According to the official documentation, subprocess.Popen deals with the underlying process creation and management.

Its usage syntax is pretty clear and straightforward. The program and its options should be placed as a sequence inside the constructor of Popen.

The following example illustrates the ls command on my Linux machine.


subprocess.Popen(["ls", "-l"])

The output of the above command.

<Popen: returncode: None args: ['ls', '-l']>

-rw-rw-r-- 1 janoroot janoroot   484 Pri  7 00:37 3
-rw-rw-r-- 1 janoroot janoroot 58744 Pri  9 11:29 content_data.html
-rw-rw-r-- 1 janoroot janoroot 58822 Pri  9 12:10 content_data.txt
-rw-rw-r-- 1 janoroot janoroot  2056 Pri  9 11:28 gather_info.py
-rw-rw-r-- 1 janoroot janoroot    46 Pri  7 02:26 research.txt
-rw-rw-r-- 1 janoroot janoroot    58 Pri  9 12:01 styles.css

The child process spawned above hangs the shell.

Standard output, input, and error can be passed to a PIPE as shown below.

from subprocess import PIPE
p = subprocess.Popen(["ls", "-l"], stdout=PIPE)

To read the output of the child process when it finishes, type the following:

   p.stdout.read()

Convert a single ebook with Python and ebook-convert


Following the syntax of the above illustrations, it is possible to convert an ebook with ebook-convert through Python.

ebook_convert = subprocess.Popen(["ebook-convert", "ebook1.epub", ".pdf"])

The conversion tool starts and prints the output on the interactive shell.




It's better to pass the standard output to a PIPE, and read from it when required.



Bulk conversion of ebooks with Python


The one-line piece of code shared above works fine for single ebook conversion. To convert multiple ebooks at the same time, multiple child processes should be opened.

A while loop comes to a solution.  As long as there are ebooks not having gone under conversion yet present in the directory, spawn a child process.

To have a general idea, the following pseudocode helps.

while ebooks:
    worker = ebook_conversion

Every ebook conversion process should be considered a worker. The script should have a maximum number of workers that can be opened at the same time. This approach guarantees the stability of the script, and also efficient consumption of machine resources.


Expressed in pseudocode, the script has the following construct.

while ebooks:
    while len(workers) < max_workers:
        worker = ebook_conversion


The current algorithm is flawed. As soon as the second while loop is entered, it keeps running as long as the number of workers is less than that of max_workers. The problem here is that there may be not enough ebooks present in the directory.

The check for the presence of ebooks should take place in the second while loop too.


while ebooks:
    while len(workers) < max_workers and ebooks:
        worker = ebook_conversion

At the moment a worker has finished its job, it should be removed from the array workers, and make way for a new one.


while ebooks:
    while len(workers) < max_workers and ebooks:
        worker = ebook_conversion 

    for worker in workers:
        if worker.is_finished:
            workers.remove(worker)


To wait for each worker to finish, the first while loop should be corrected as shown below.

while ebooks or workers:
    while len(workers) < max_workers and ebooks:
        worker = ebook_conversion

    for worker in workers:
        if worker.is_finished:
            workers.remove(worker)


Coding the script in Python


The above pseudocode serves as a precious guide to write the Python code for the script.  


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
while ebooks or workers:
    while len(workers) < 3 and ebooks:
        ebook = ebooks[0]
        ebooks = ebooks[1:]
        ebook_converted = ".".join([base_title, format_to])
        normalized_ebook_title = normalize_title(ebook)
        conversion = subprocess.Popen(
                       ["ebook-convert",
                        abs_path(normalized_ebook_title),
                        abs_path(ebook_converted)],
                       stdout=subprocess.PIPE, encoding="utf-8")

    for w in workers:
        if w["worker"].poll() is not None:
            workers.remove(w)




Having a single look at the above code, it is self-explanatory that each ebook that goes under conversion is popped out of the array.


Two helper functions, normalize_title, and abs_path are easily noticed. The first one cleanses an ebook title while the second creates the absolute path of a document based on its root directory.


The method poll() is part of the instance Popen. It checks if the child process has terminated and sends back the returncode attribute. On success, the value of the returncode is 0.

The following is a more complete piece of code.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import os
import subprocess



def abs_path(ebook):
    """
    Returns abs path of ebook based
    on the ebook_dir.
    """

    return "%s/%s" % (ebook_dir, ebook)


def normalize_title(title):  # TODO replace space with _
    normalized_title = []
    for idx, el in enumerate(title):
        if el == "." and idx != len(title) - 5:  # - len(format) + 1
            continue

        normalized_title.append(el)

    return "".join(normalized_title)


ebooks = os.listdir(ebook_dir)
workers = []
worker = {}


while ebooks or workers:
    while len(workers) < 3 and ebooks:
        ebook = ebooks[0]
        ebooks = ebooks[1:]
        normalized_ebook_title = normalize_title(ebook)
        base_title = normalized_ebook_title.split(".")[0]
        ebook_converted = ".".join([base_title, format_to])
        
        conversion = subprocess.Popen(
                       ["ebook-convert",
                        abs_path(normalized_ebook_title),
                        abs_path(ebook_converted)],
                       stdout=subprocess.PIPE, encoding="utf-8")
        
        worker["worker"] = conversion
        worker["file_to_convert"] = normalized_ebook_title
        worker["converted_file"] = ebook_converted

        workers.append(worker)

        worker = {}


    for w in workers:
        if w["worker"].poll() is not None:
            workers.remove(w) 

Necessary modules are imported inside the script. It's very important to notice the dictionary worker; it helps to keep track of each conversion data, which can be used for post-processing.

The moment the worker successfully finishes its job, a tag should be added to the old document, with the main purpose of discerning it in the future.

 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import os
import subprocess



def abs_path(ebook):
    """
    Returns abs path of ebook based
    on the ebook_dir.
    """

    return "%s/%s" % (ebook_dir, ebook)


def normalize_title(title):  # TODO replace space with _
    normalized_title = []
    for idx, el in enumerate(title):
        if el == "." and idx != len(title) - 5:  # - len(format) + 1
            continue

        normalized_title.append(el)

    return "".join(normalized_title)


# tag
tag = "__CONVERTED__."

ebooks = os.listdir(ebook_dir)
workers = []
worker = {}


while ebooks or workers:
    while len(workers) < 3 and ebooks:
        ebook = ebooks[0]
        ebooks = ebooks[1:]
        normalized_ebook_title = normalize_title(ebook)
        base_title = normalized_ebook_title.split(".")[0]
        ebook_converted = ".".join([base_title, format_to])
        
        conversion = subprocess.Popen(
                       ["ebook-convert",
                        abs_path(normalized_ebook_title),
                        abs_path(ebook_converted)],
                       stdout=subprocess.PIPE, encoding="utf-8")
        
        worker["worker"] = conversion
        worker["file_to_convert"] = normalized_ebook_title
        worker["converted_file"] = ebook_converted

        workers.append(worker)

        worker = {}


    for w in workers:
        if w["worker"].poll() is not None:
     
            converted_title = tag.join([w["file_to_convert"].split(".")[0],
                                        w["file_to_convert"].split(".")[1]])
            os.rename(abs_path(w["file_to_convert"]),
                      abs_path(converted_title))
            workers.remove(w) 


The above script does the job. With the main purpose of testing it, two global variables should be defined. The format_to and the ebook_dir.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import os
import subprocess



def abs_path(ebook):
    """
    Returns abs path of ebook based
    on the ebook_dir.
    """

    return "%s/%s" % (ebook_dir, ebook)


def normalize_title(title):  # TODO replace space with _
    normalized_title = []
    for idx, el in enumerate(title):
        if el == "." and idx != len(title) - 5:  # - len(format) + 1
            continue

        normalized_title.append(el)

    return "".join(normalized_title)


# tag
tag = "__CONVERTED__."

format_to = "pdf"
ebook_dir = "/home/janoroot/Desktop/ebooks_to_conv/"


ebooks = os.listdir(ebook_dir)
workers = []
worker = {}


while ebooks or workers:
    while len(workers) < 3 and ebooks:
        ebook = ebooks[0]
        ebooks = ebooks[1:]
        normalized_ebook_title = normalize_title(ebook)
        base_title = normalized_ebook_title.split(".")[0]
        ebook_converted = ".".join([base_title, format_to])
        
        conversion = subprocess.Popen(
                       ["ebook-convert",
                        abs_path(normalized_ebook_title),
                        abs_path(ebook_converted)],
                       stdout=subprocess.PIPE, encoding="utf-8")
        
        worker["worker"] = conversion
        worker["file_to_convert"] = normalized_ebook_title
        worker["converted_file"] = ebook_converted

        workers.append(worker)

        worker = {}


    for w in workers:
        if w["worker"].poll() is not None:
     
            converted_title = tag.join([w["file_to_convert"].split(".")[0],
                                        w["file_to_convert"].split(".")[1]])
            os.rename(abs_path(w["file_to_convert"]),
                      abs_path(converted_title))
            workers.remove(w) 

Assign the absolute path of your directory of ebooks to the variable ebook_dir and also specify the format for your bulk conversion.  My directory has materials in the epub format, ready to be converted to pdf.

Then run the script with Python 3.


python3 ebook_conversion.py


As soon as the bulk conversion is finished, you can view the new files present in the directory of ebooks as specified in the script. 



Problems with the script:

  • The directory of ebooks for conversion and format of the new documents are specified manually.
  • There is no option for purging the old documents.
  • There is no option to place the converted files into another directory.
  • Already processed documents can be processed again.
  • Function normalize_title works only for .epub files.


Add command-line interface to the script


The command-line interface helps to automate manual work. Through its arguments, data that is useful to the script can be specified. 

The argparse is a built-in module in Python, designed to make the writing of user-friendly command-line interfaces easy.



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import os, argparse
import subprocess


# cli interface

cli_parser = argparse.ArgumentParser(description="Ebook converter.")
cli_parser.add_argument("--purge", dest="purge", action="store_true",
                        help="Purge the ebooks after conversion")
cli_parser.add_argument("--format", dest="format_to", type=str, default="pdf",
                        help="Specifies the fomat to convert the ebook to.")

# add argument for an arbitrary directory
cli_parser.add_argument("--ebook-dir", dest="ebookdir", type=str,
                        help="Specifies the directory with ebooks.")

# add argument for output directory
cli_parser.add_argument("--output-dir", dest="outputdir", type=str,
                        help="Specifies the output directory.")

cli_args = cli_parser.parse_args()


def abs_path(ebook):
    """
    Returns abs path of ebook based
    on the ebook_dir.
    """

    return "%s/%s" % (ebook_dir, ebook)


def normalize_title(title):  # TODO replace space with _
    normalized_title = []
    for idx, el in enumerate(title):
        if el == "." and idx != len(title) - 5:  # - len(format) + 1
            continue

        normalized_title.append(el)

    return "".join(normalized_title)


# tag
tag = "__CONVERTED__."

format_to = "pdf"
ebook_dir = "/home/janoroot/Desktop/ebooks_to_conv/"


ebooks = os.listdir(ebook_dir)
workers = []
worker = {}


while ebooks or workers:
    while len(workers) < 3 and ebooks:
        ebook = ebooks[0]
        ebooks = ebooks[1:]
        normalized_ebook_title = normalize_title(ebook)
        base_title = normalized_ebook_title.split(".")[0]
        ebook_converted = ".".join([base_title, format_to])
        
        conversion = subprocess.Popen(
                       ["ebook-convert",
                        abs_path(normalized_ebook_title),
                        abs_path(ebook_converted)],
                       stdout=subprocess.PIPE, encoding="utf-8")
        
        worker["worker"] = conversion
        worker["file_to_convert"] = normalized_ebook_title
        worker["converted_file"] = ebook_converted

        workers.append(worker)

        worker = {}


    for w in workers:
        if w["worker"].poll() is not None:
     
            converted_title = tag.join([w["file_to_convert"].split(".")[0],
                                        w["file_to_convert"].split(".")[1]])
            os.rename(abs_path(w["file_to_convert"]),
                      abs_path(converted_title))
            workers.remove(w) 

The directory of ebooks for conversion and the format of the new files can now be specified directly when running the script on the command line.

python3 ebook_conversion.py --ebook-dir /home/janoroot/ebooks_to_convert --format pdf

The script doesn't process them still. Each of their value can be accessed using the following syntax inside the script.

cli_args.arg_name_here

To access the value of the format, type:

cli_args.format


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import os
import argparse
import subprocess


# cli interface

cli_parser = argparse.ArgumentParser(description="Ebook converter.")
cli_parser.add_argument("--purge", dest="purge", action="store_true",
                        help="Purge the ebooks after conversion")
cli_parser.add_argument("--format", dest="format_to", type=str, default="pdf",
                        help="Specifies the fomat to convert the ebook to.")

# add argument for an arbitrary directory
cli_parser.add_argument("--ebook-dir", dest="ebookdir", type=str,
                        help="Specifies the directory with ebooks.")

# add argument for output directory
cli_parser.add_argument("--output-dir", dest="outputdir", type=str,
                        help="Specifies the output directory.")

cli_args = cli_parser.parse_args()


format_to = cli_args.format_to

if cli_args.ebookdir is not None:
    ebook_dir = cli_args.ebookdir
else:
    ebook_dir = os.path.abspath("./ebooks/")


original_ebook_dir = ebook_dir


if ebook_dir.endswith("/"):
    ebook_dir = ebook_dir.rstrip("/")


def abs_path(ebook):
    """
    Returns abs path of ebook based
    on the ebook_dir.
    """

    return "%s/%s" % (ebook_dir, ebook)


def normalize_title(title):  # TODO replace space with _
    normalized_title = []
    for idx, el in enumerate(title):
        if el == "." and idx != len(title) - 5:  # - len(format) + 1
            continue

        normalized_title.append(el)

    return "".join(normalized_title)


# tag
tag = "__CONVERTED__."


ebooks = os.listdir(ebook_dir)
workers = []
worker = {}


while ebooks or workers:
    while len(workers) < 3 and ebooks:
        ebook = ebooks[0]
        ebooks = ebooks[1:]
        normalized_ebook_title = normalize_title(ebook)
        base_title = normalized_ebook_title.split(".")[0]
        ebook_converted = ".".join([base_title, format_to])
        
        conversion = subprocess.Popen(
                       ["ebook-convert",
                        abs_path(normalized_ebook_title),
                        abs_path(ebook_converted)],
                       stdout=subprocess.PIPE, encoding="utf-8")
        
        worker["worker"] = conversion
        worker["file_to_convert"] = normalized_ebook_title
        worker["converted_file"] = ebook_converted

        workers.append(worker)

        worker = {}


    for w in workers:
        if w["worker"].poll() is not None:
     
            converted_title = tag.join([w["file_to_convert"].split(".")[0],
                                        w["file_to_convert"].split(".")[1]])
            os.rename(abs_path(w["file_to_convert"]),
                      abs_path(converted_title))
            workers.remove(w) 

Now it is possible to convert ebooks without the need of having the format and the directory hardcoded inside the script.
python3 ebook_conversion.py --ebook-dir /home/janoroot/ebooks_to_convert --format pdf
Note: Make sure to specify the absolute path of the directory that contains the ebooks for conversion.


Since the default format is pdf, the following command works too.

python3 ebook_conversion.py --ebook-dir /home/janoroot/ebooks_to_convert

 

Final script


After some hours of work dedicated to the improvement and new features, I ended up with the following script.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#!/usr/bin/python3

import logging
import argparse
import time
import os
import shutil
import subprocess


# configure the logger
logger = logging.getLogger("ebook-convert")
logger.setLevel(logging.INFO)

# create the logging file handler
fh = logging.FileHandler("ebook_convert.log")

# create a formatter

formatter = logging.Formatter("%(asctime)s - %(name)s \
                              - %(levelname)s - %(message)s")
fh.setFormatter(formatter)

# add file handler to logger
logger.addHandler(fh)

# cli interface

cli_parser = argparse.ArgumentParser(description="Ebook converter.")
cli_parser.add_argument("--purge", dest="purge", action="store_true",
                        help="Purge the ebooks after conversion")
cli_parser.add_argument("--format", dest="format_to", type=str, default="pdf",
                        help="Specifies the fomat to convert the ebook to.")

# add argument for an arbitrary directory
cli_parser.add_argument("--ebook-dir", dest="ebookdir", type=str,
                        help="Specifies the directory with ebooks.")

# add argument for output directory
cli_parser.add_argument("--output-dir", dest="outputdir", type=str,
                        help="Specifies the output directory.")

cli_args = cli_parser.parse_args()


# tag
tag = "__CONVERTED__."


if cli_args.outputdir is not None:
    outputdir = os.path.abspath(cli_args.outputdir)
else:
    outputdir = None


if outputdir and not os.path.exists(outputdir):
    os.mkdir(outputdir)

if cli_args.purge:
    purge = True
else:
    purge = False


format_to = cli_args.format_to


if cli_args.ebookdir is not None:
    ebook_dir = cli_args.ebookdir
else:
    ebook_dir = os.path.abspath("./ebooks/")


original_ebook_dir = ebook_dir

if ebook_dir.endswith("/"):
    ebook_dir = ebook_dir.rstrip("/")


def abs_path(ebook):
    """
    Returns abs path of ebook based
    on the ebook_dir.
    """

    return "%s/%s" % (ebook_dir, ebook)


def normalize_title(title):  # TODO replace space with _
    normalized_title = []
    for idx, el in enumerate(title):
        if el == "." and idx != len(title) - 5:  # - len(format) + 1
            continue

        normalized_title.append(el)

    return "".join(normalized_title)


ebooks = os.listdir(ebook_dir)
workers = []
worker = {}


while ebooks or workers:
    while len(workers) < 3 and ebooks:
        ebook = ebooks[0]
        ebooks = ebooks[1:]
        if tag.strip(".") not in ebook:
            print("Converting %s  to %s..." % (ebook, format_to))
            normalized_ebook_title = normalize_title(ebook)
            base_title = normalized_ebook_title.split(".")[0]

            # give the ebook a cleaner name
            os.rename(abs_path(ebook), abs_path(normalized_ebook_title))

            ebook_converted = ".".join([base_title, format_to])
            conversion = subprocess.Popen(
                       ["ebook-convert",
                        abs_path(normalized_ebook_title),
                        abs_path(ebook_converted)],
                       stdout=subprocess.PIPE, encoding="utf-8")

            worker["start_time"] = time.time()
            worker["worker"] = conversion
            worker["file_to_convert"] = normalized_ebook_title
            worker["converted_file"] = ebook_converted

            workers.append(worker)

            worker = {}

    for w in workers:
        poll_status = w["worker"].poll()

        if poll_status is not None and poll_status == 0:
            delta = time.time() - w["start_time"]
            print("%s done in %f seconds." % (w["file_to_convert"], delta)) 
            logger.info("Converted %s to %s" % (w["file_to_convert"],
                                                format_to))
            converted_title = tag.join([w["file_to_convert"].split(".")[0],
                                        w["file_to_convert"].split(".")[1]])
            os.rename(abs_path(w["file_to_convert"]),
                      abs_path(converted_title))
            
            if outputdir is not None: 
                shutil.move(abs_path(w["converted_file"]), outputdir)
                logger.info("Moved %s to %s." % (w["converted_file"], outputdir))
            if purge:
                os.remove(abs_path(converted_title))
            workers.remove(w)

 
New features and improvements:

  • When --purge is specified the old ebooks are removed from the filesystem.
  • The activity of the script is now being logged inside a file called ebook_convert.log
  • Duration in seconds is displayed at the end of each ebook conversion.
  • The --output-dir option helps to specify an output directory for converted ebooks.

To convert a list of ebooks and have the converted files in another directory, use the --output-dir. 



Directory with ebooks for conversion.



python3 ebook_conversion.py --ebook-dir /home/janoroot/Desktop/ebooks_to_conv/ --output-dir /home/janoroot/Desktop/pdf_ebooks/


Converted ebooks inside the output directory.


The --purge argument helps to purge the already processed files.

python3 ebook_conversion.py --ebook-dir /home/janoroot/Desktop/ebooks_to_conv/ --output-dir /home/janoroot/Desktop/pdf_ebooks/ --purge



Final thoughts


Through this article, you learned how to write a script that does ebook conversion by using Calibre's ebook-convert tool as the processing engine. Modules such as logging, os, shutil, and subprocess were used for the construction of the script. 



Although the tool is still in a beta version, it offers a free solution to anyone who wants to convert documents in bulk.


Share on Google Plus

About Oltjano Backa

Always curious on how stuff works under the hood, hacking has always been my way of materializing the true essence of my soul in the third dimension. My life as a hacker got really interesting when I discovered Python and Ubuntu Linux. Thirsty for knowledge, open-source technologies are my main toys.

0 Comments:

Post a Comment