/* This file is part of Cinaest.
*
* Copyright (C) 2010 Philipp Zabel
*
* Cinaest is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Cinaest is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Cinaest. If not, see .
*/
namespace Hildon {
public const int MARGIN_DOUBLE = 16;
public const int MARGIN_HALF = 4;
}
namespace Poster {
public const int SMALL_WIDTH = (800 - 2*Hildon.MARGIN_DOUBLE - 4*Hildon.MARGIN_HALF)/5;
public const int SMALL_HEIGHT = (420 - Hildon.MARGIN_HALF)/2;
public const int ICON_WIDTH = 46;
public const int ICON_HEIGHT = 64;
}
// A single IMDb poster image search (parsing and retrieval of the poster image URI)
public class IMDbMessage : Soup.Message {
public string title;
public int year;
public string poster_uri = null;
private bool thumbnail;
public IMDbMessage (string title_, int year_, bool thumbnail_ = false) {
Object (method: "GET");
title = title_;
year = year_;
thumbnail = thumbnail_;
string url = "http://www.imdb.com/find?s=tt&q=%s (%d)".printf (convert (title, -1, "iso8859-1", "utf-8"), year);
uri = new Soup.URI (url);
}
}
// Encapsulation of a single poster download (IMDb title search query and image file download)
public class IMDbPosterDownload : Object {
private IMDbPosterDownloader downloader;
private Soup.Session session;
private string cache_dir;
private string cache_filename;
private bool thumbnail;
private bool cancelled = false;
public int handle;
public IMDbPosterDownload (string title, string year, bool thumbnail_, int _handle, IMDbPosterDownloader _downloader) {
handle = _handle;
downloader = _downloader;
session = downloader.session;
thumbnail = thumbnail_;
var message = new IMDbMessage (title, year.to_int (), thumbnail);
session.queue_message (message, title_page_callback);
if (thumbnail) {
// FIXME
cache_dir = Path.build_filename (Environment.get_tmp_dir(), "cinaest-thumbnails");
} else {
cache_dir = Path.build_filename (Environment.get_user_cache_dir(), "media-art");
}
cache_filename = "movie-" +
Checksum.compute_for_string (ChecksumType.MD5, (title).down ()) + "-" +
Checksum.compute_for_string (ChecksumType.MD5, (year).down ()) + ".jpeg";
}
private void title_page_callback (Soup.Session session, Soup.Message message) {
if (cancelled ||
message.status_code != Soup.KnownStatusCode.OK) {
print ("[%d] NO POSTER FOR %s (CODE %u)\n", handle, message.uri.to_string (false), message.status_code);
downloader.finished (this, null);
return;
}
if (message.uri.path == "/find") {
print ("[%d] AMBIGUOUS RESULTS: %s\n", handle, message.uri.to_string (false));
var msg = (IMDbMessage) message;
var re3 = new Regex ("]*>([^<]*) *\\(([0-9]*)");
MatchInfo match;
if (re3.match ((string) message.response_body.data, 0, out match)) {
do {
print ("[%d] POTENTIAL RESULT: %s (%s)\n", handle, match.fetch (2), match.fetch (3));
if (msg.title.down () == match.fetch (2).down () &&
msg.year == match.fetch (3).to_int ()) {
string url = "http://www.imdb.com" + match.fetch (1);
print ("[%d] CHOSE RESULT URL: %s\n", handle, url);
message.uri = new Soup.URI (url);
session.queue_message (message, this.title_page_callback);
return;
}
} while (match.next ());
}
print ("[%d] NO MATCH\n", handle);
downloader.finished (this, null);
return;
}
print ("[%d] GOT TITLE PAGE FOR %s\n", handle, message.uri.path);
MatchInfo match;
if (downloader.re1.match ((string) message.response_body.data, 0, out match)) {
string url = "http://www.imdb.com" + match.fetch (1);
print ("[%d] FOUND PHOTO PAGE URL: %s\n", handle, url);
message.uri = new Soup.URI (url);
session.queue_message (message, this.photo_page_callback);
} else {
print ("[%d] NO POSTER AVAILABLE\n", handle);
downloader.finished (this, null);
}
}
private void photo_page_callback (Soup.Session session, Soup.Message message) {
if (cancelled ||
message.status_code != Soup.KnownStatusCode.OK) {
downloader.finished (this, null);
return;
}
print ("[%d] GOT PHOTO PAGE %s\n", handle, message.uri.path);
MatchInfo match;
if (downloader.re2.match ((string) message.response_body.data, 0, out match)) {
string url = match.fetch (1);
print ("[%d] FOUND IMAGE URL: %s\n", handle, url);
message.uri = new Soup.URI (url);
session.queue_message (message, this.image_callback);
}
}
private void image_callback (Soup.Session session, Soup.Message message) {
if (cancelled ||
message.status_code != Soup.KnownStatusCode.OK) {
downloader.finished (this, null);
return;
}
print ("[%d] Downloaded poster: %s\n", handle, message.uri.to_string (false));
// Define cache path according to the Media Art Storage Spec (http://live.gnome.org/MediaArtStorageSpec)
string cache_path = Path.build_filename (cache_dir, cache_filename);
// Make sure the directory .album_arts is available
DirUtils.create_with_parents (cache_dir, 0770);
if (FileUtils.set_contents (cache_path + ".part",
(string) message.response_body.data,
(ssize_t) message.response_body.length)) {
if (!thumbnail) {
FileUtils.rename (cache_path + ".part", cache_path);
} else {
// Scale and crop
var p1 = new Gdk.Pixbuf.from_file (cache_path + ".part");
var p2 = new Gdk.Pixbuf (Gdk.Colorspace.RGB, true, 8, Poster.SMALL_WIDTH, Poster.SMALL_HEIGHT);
double xscale = (double)Poster.SMALL_WIDTH / (double)p1.width;
double yscale = (double)Poster.SMALL_HEIGHT / (double)p1.height;
double scale;
int ofs_x = 0;
int ofs_y = 0;
if (xscale > yscale) {
scale = xscale;
ofs_y = -(int) (p1.height*scale - Poster.SMALL_HEIGHT)/2;
} else {
scale = yscale;
ofs_x = -(int) (p1.width*scale - Poster.SMALL_WIDTH)/2;
}
p1.scale (p2, 0, 0, Poster.SMALL_WIDTH, Poster.SMALL_HEIGHT, ofs_x, ofs_y, scale, scale, Gdk.InterpType.BILINEAR);
print ("[%d] Scaled %d x %d --> %d x %d (%+d,%+d)\n", handle, p1.width, p1.height, p2.width, p2.height, ofs_x, ofs_y);
p2.save (cache_path, "png", null);
FileUtils.unlink (cache_path + ".part");
}
print ("[%d] Stored as: %s\n", handle, cache_path);
} else {
stdout.printf ("[%d] Failed to store poster\n", handle);
}
downloader.finished (this, cache_path);
}
public void cancel () {
print ("[%d] Cancelled\n", handle);
cancelled = true;
}
}
// The D-Bus service to manage poster downloads
public class IMDbPosterDownloader : Object, PosterDownloader {
private MainLoop loop;
private int fetch_handle = 1;
private List downloads = null;
private uint source_id;
public Soup.SessionAsync session;
public Regex re1;
public Regex re2;
public IMDbPosterDownloader () {
loop = new MainLoop (null);
session = new Soup.SessionAsync ();
session.max_conns = 40;
session.max_conns_per_host = 20;
try {
re1 = new Regex ("\"(/rg/action-box-title/primary-photo/media/[^\"]*)\"");
re2 = new Regex ("\"(http://ia.media-imdb.com/images[^\"]*)\"");
} catch (RegexError e) {
}
}
public void timeout_quit () {
// With every change we reset the timer to 3min
if (source_id != 0) {
Source.remove (source_id);
}
source_id = Timeout.add_seconds (180, quit);
}
private bool quit () {
loop.quit ();
print ("Timeout. Quitting with %u remaining downloads.\n", downloads.length ());
foreach (IMDbPosterDownload download in downloads)
failed (download.handle);
// One-shot only
return false;
}
public void run () {
loop.run ();
}
public void finished (IMDbPosterDownload download, string? cache_path) {
if (cache_path != null)
fetched (download.handle, cache_path);
else
failed (download.handle);
downloads.remove (download);
timeout_quit ();
}
// Implement the PosterDownloader interface
public int Fetch (string title, string year, string kind) throws DBus.Error {
print ("Fetch (\"%s\", \"%s\", \"%s\") = %d\n", title, year, kind, fetch_handle+1);
var download = new IMDbPosterDownload (title, year, false, ++fetch_handle, this);
downloads.append (download);
return fetch_handle;
}
public int FetchThumbnail (string title, string year, string kind) throws DBus.Error {
print ("FetchThumbnail (\"%s\", \"%s\", \"%s\") = %d\n", title, year, kind, fetch_handle+1);
var download = new IMDbPosterDownload (title, year, true, ++fetch_handle, this);
downloads.append (download);
return fetch_handle;
}
public void Unqueue (int handle) throws DBus.Error {
print ("Unqueue (%d)\n", handle);
IMDbPosterDownload download = null;
foreach (IMDbPosterDownload d in downloads) {
if (d.handle == handle) {
download = d;
d.cancel ();
break;
}
}
if (download != null) {
downloads.remove (download);
}
}
static void main () {
try {
var conn = DBus.Bus.get (DBus.BusType.SESSION);
dynamic DBus.Object bus = conn.get_object ("org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus");
// Try to register service in session bus
uint res = bus.request_name ("org.maemo.movieposter.IMDb", (uint) 0);
if (res == DBus.RequestNameReply.PRIMARY_OWNER) {
// Start server
var server = new IMDbPosterDownloader ();
conn.register_object ("/org/maemo/movieposter/IMDb", server);
server.timeout_quit ();
server.run ();
}
} catch (Error e) {
error ("Oops: %s\n", e.message);
}
}
}