[grilo-plugins] local-metadata: add series support



commit 46f707538bd5bfb0d36b9c2701f7bccafdc63dfd
Author: Lionel Landwerlin <lionel g landwerlin linux intel com>
Date:   Sun Mar 20 03:39:06 2011 +0000

    local-metadata: add series support
    
    Based on a patch for Tracker by Iain Holmes :
    
    http://build.meego.com/package/view_file?file=0002-Tracker-extract-Parse-the-video-filename-to-obtain-e.patch&package=tracker&project=devel%3Acontentfw&srcmd5=c91b724488ee9ae476a9ce65755d8152
    
    Signed-off-by: Lionel Landwerlin <lionel g landwerlin linux intel com>

 configure.ac                                     |    3 +
 src/metadata/local-metadata/grl-local-metadata.c |  489 ++++++++++++++++++++--
 src/metadata/local-metadata/grl-local-metadata.h |    6 +-
 3 files changed, 471 insertions(+), 27 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 5d9d9a9..15304f8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -709,6 +709,9 @@ AC_ARG_ENABLE(localmetadata,
                         if test "x$HAVE_GIO" = "xno"; then
                            AC_MSG_ERROR([gio-2.0 not found, install it or use --disable-localmetadata])
                         fi
+                        if test "x$HAVE_GLIB_2_26" = "xno"; then
+                           AC_MSG_ERROR([glib-2.26.0 or above not found, install it or use --disable-localmetadata])
+                        fi
                         ;;
                 esac
         ],
diff --git a/src/metadata/local-metadata/grl-local-metadata.c b/src/metadata/local-metadata/grl-local-metadata.c
index 62737ba..9bba09f 100644
--- a/src/metadata/local-metadata/grl-local-metadata.c
+++ b/src/metadata/local-metadata/grl-local-metadata.c
@@ -24,6 +24,9 @@
 #include "config.h"
 #endif
 
+#include <string.h>
+#include <stdlib.h>
+
 #include <grilo.h>
 #include <gio/gio.h>
 
@@ -42,8 +45,65 @@ GRL_LOG_DOMAIN_STATIC(local_metadata_log_domain);
 #define LICENSE     "LGPL"
 #define SITE        "http://www.igalia.com";
 
-
-static GrlLocalMetadataSource *grl_local_metadata_source_new (void);
+/**/
+
+#define TV_REGEX                                \
+  "(?<showname>.*)\\."                          \
+  "(?<season>(?:\\d{1,2})|(?:[sS]\\K\\d{1,2}))" \
+  "(?<episode>(?:\\d{2})|(?:[eE]\\K\\d{1,2}))"  \
+  "\\.?(?<name>.*)?"
+#define MOVIE_REGEX                             \
+  "(?<name>.*)"                                 \
+  "\\.?[\\(\\[](?<year>[12][90]\\d{2})[\\)\\]]"
+
+/**/
+
+#define GRL_LOCAL_METADATA_SOURCE_GET_PRIVATE(object)           \
+  (G_TYPE_INSTANCE_GET_PRIVATE((object),                        \
+                               GRL_LOCAL_METADATA_SOURCE_TYPE,	\
+                               GrlLocalMetadataSourcePriv))
+
+enum {
+  PROP_0,
+  PROP_GUESS_VIDEO,
+};
+
+struct _GrlLocalMetadataSourcePriv {
+  gboolean guess_video;
+};
+
+/**/
+
+typedef enum {
+  FLAG_VIDEO_TITLE    = 0x1,
+  FLAG_VIDEO_SHOWNAME = 0x2,
+  FLAG_VIDEO_DATE     = 0x4,
+  FLAG_VIDEO_SEASON   = 0x8,
+  FLAG_VIDEO_EPISODE  = 0x10,
+  FLAG_THUMBNAIL      = 0x20,
+} resolution_flags_t;
+
+const gchar *video_blacklisted_prefix[] = {
+  "tpz-", NULL
+};
+
+const char *video_blacklisted_words[] = {
+  "720p", "1080p",
+  "ws", "WS", "proper", "PROPER",
+  "repack", "real.repack",
+  "hdtv", "HDTV", "pdtv", "PDTV", "notv", "NOTV",
+  "dsr", "DSR", "DVDRip", "divx", "DIVX", "xvid", "Xvid",
+  NULL
+};
+
+/**/
+
+static void grl_local_metadata_source_set_property (GObject      *object,
+                                                    guint         propid,
+                                                    const GValue *value,
+                                                    GParamSpec   *pspec);
+
+static GrlLocalMetadataSource *grl_local_metadata_source_new (gboolean guess_video);
 
 static void grl_local_metadata_source_resolve (GrlMetadataSource *source,
                                               GrlMetadataSourceResolveSpec *rs);
@@ -59,6 +119,7 @@ gboolean grl_local_metadata_source_plugin_init (GrlPluginRegistry *registry,
                                                const GrlPluginInfo *plugin,
                                                GList *configs);
 
+/**/
 
 /* =================== GrlLocalMetadata Plugin  =============== */
 
@@ -67,11 +128,28 @@ grl_local_metadata_source_plugin_init (GrlPluginRegistry *registry,
                                       const GrlPluginInfo *plugin,
                                       GList *configs)
 {
+  guint config_count;
+  gboolean guess_video = TRUE;
+  GrlConfig *config;
+
   GRL_LOG_DOMAIN_INIT (local_metadata_log_domain, "local-metadata");
 
   GRL_DEBUG ("grl_local_metadata_source_plugin_init");
 
-  GrlLocalMetadataSource *source = grl_local_metadata_source_new ();
+  if (!configs) {
+    GRL_WARNING ("\tConfiguration not provided! Using default configuration.");
+  } else {
+    config_count = g_list_length (configs);
+    if (config_count > 1) {
+      GRL_WARNING ("\tProvided %i configs, but will only use one", config_count);
+    }
+
+    config = GRL_CONFIG (configs->data);
+
+    guess_video = grl_config_get_boolean (config, "guess-video");
+  }
+
+  GrlLocalMetadataSource *source = grl_local_metadata_source_new (guess_video);
   grl_plugin_registry_register_source (registry,
                                        plugin,
                                        GRL_MEDIA_PLUGIN (source),
@@ -86,23 +164,40 @@ GRL_PLUGIN_REGISTER (grl_local_metadata_source_plugin_init,
 /* ================== GrlLocalMetadata GObject ================ */
 
 static GrlLocalMetadataSource *
-grl_local_metadata_source_new (void)
+grl_local_metadata_source_new (gboolean guess_video)
 {
   GRL_DEBUG ("grl_local_metadata_source_new");
   return g_object_new (GRL_LOCAL_METADATA_SOURCE_TYPE,
 		       "source-id", SOURCE_ID,
 		       "source-name", SOURCE_NAME,
 		       "source-desc", SOURCE_DESC,
+                       "guess-video", guess_video,
 		       NULL);
 }
 
 static void
 grl_local_metadata_source_class_init (GrlLocalMetadataSourceClass * klass)
 {
+  GObjectClass           *g_class        = G_OBJECT_CLASS (klass);
   GrlMetadataSourceClass *metadata_class = GRL_METADATA_SOURCE_CLASS (klass);
+
   metadata_class->supported_keys = grl_local_metadata_source_supported_keys;
   metadata_class->may_resolve = grl_local_metadata_source_may_resolve;
   metadata_class->resolve = grl_local_metadata_source_resolve;
+
+  g_class->set_property = grl_local_metadata_source_set_property;
+
+  g_object_class_install_property (g_class,
+                                   PROP_GUESS_VIDEO,
+                                   g_param_spec_boolean ("guess-video",
+                                                         "Guess video",
+                                                         "Guess video metadata "
+                                                         "from filename",
+                                                         TRUE,
+                                                         G_PARAM_WRITABLE |
+                                                         G_PARAM_CONSTRUCT_ONLY));
+
+  g_type_class_add_private (klass, sizeof (GrlLocalMetadataSourcePriv));
 }
 
 static void
@@ -114,7 +209,211 @@ G_DEFINE_TYPE (GrlLocalMetadataSource,
                grl_local_metadata_source,
                GRL_TYPE_METADATA_SOURCE);
 
+static void
+grl_local_metadata_source_set_property (GObject      *object,
+                                        guint         propid,
+                                        const GValue *value,
+                                        GParamSpec   *pspec)
+{
+  GrlLocalMetadataSourcePriv *priv =
+    GRL_LOCAL_METADATA_SOURCE_GET_PRIVATE (object);
+
+  switch (propid) {
+  case PROP_GUESS_VIDEO:
+    priv->guess_video = g_value_get_boolean (value);
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, propid, pspec);
+  }
+}
+
 /* ======================= Utilities ==================== */
+
+static gchar *
+video_sanitise_string (const gchar *str)
+{
+  int    i;
+  gchar *line;
+
+  line = (gchar *) str;
+  for (i = 0; video_blacklisted_prefix[i]; i++) {
+    if (g_str_has_prefix (str, video_blacklisted_prefix[i])) {
+      int len = strlen (video_blacklisted_prefix[i]);
+
+      line = (gchar *) str + len;
+    }
+  }
+
+  for (i = 0; video_blacklisted_words[i]; i++) {
+    gchar *end;
+
+    end = strstr (line, video_blacklisted_words[i]);
+    if (end) {
+      return g_strndup (line, end - line);
+    }
+  }
+
+  return g_strdup (line);
+}
+
+/* tidies strings before we run them through the regexes */
+static gchar *
+video_uri_to_metadata (const gchar *uri)
+{
+  gchar *ext, *basename, *name, *whitelisted;
+
+  basename = g_path_get_basename (uri);
+  ext = strrchr (basename, '.');
+  if (ext) {
+    name = g_strndup (basename, ext - basename);
+    g_free (basename);
+  } else {
+    name = basename;
+  }
+
+  /* Replace _ <space> with . */
+  g_strdelimit (name, "_ ", '.');
+  whitelisted = video_sanitise_string (name);
+  g_free (name);
+
+  return whitelisted;
+}
+
+static void
+video_guess_values_from_uri (const gchar *uri,
+                             gchar      **title,
+                             gchar      **showname,
+                             GDateTime  **date,
+                             gint        *season,
+                             gint        *episode)
+{
+  gchar      *metadata;
+  GRegex     *regex;
+  GMatchInfo *info;
+
+  metadata = video_uri_to_metadata (uri);
+
+  regex = g_regex_new (MOVIE_REGEX, 0, 0, NULL);
+  g_regex_match (regex, metadata, 0, &info);
+
+  if (g_match_info_matches (info)) {
+    if (title) {
+      *title = g_match_info_fetch_named (info, "name");
+      /* Replace "." with <space> */
+      g_strdelimit (*title, ".", ' ');
+    }
+
+    if (date) {
+      gchar *year = g_match_info_fetch_named (info, "year");
+
+      *date = g_date_time_new_utc (atoi (year), 1, 1, 0, 0, 0.0);
+      g_free (year);
+    }
+
+    if (showname) {
+      *showname = NULL;
+    }
+
+    if (season) {
+      *season = 0;
+    }
+
+    if (episode) {
+      *episode = 0;
+    }
+
+    g_regex_unref (regex);
+    g_match_info_free (info);
+    g_free (metadata);
+
+    return;
+  }
+
+  g_regex_unref (regex);
+  g_match_info_free (info);
+
+  regex = g_regex_new (TV_REGEX, 0, 0, NULL);
+  g_regex_match (regex, metadata, 0, &info);
+
+  if (g_match_info_matches (info)) {
+    if (title) {
+      *title = g_match_info_fetch_named (info, "name");
+      g_strdelimit (*title, ".", ' ');
+    }
+
+    if (showname) {
+      *showname = g_match_info_fetch_named (info, "showname");
+      g_strdelimit (*showname, ".", ' ');
+    }
+
+    if (season) {
+      gchar *s = g_match_info_fetch_named (info, "season");
+      if (s) {
+        if (*s == 's' || *s == 'S') {
+          *season = atoi (s + 1);
+        } else {
+          *season = atoi (s);
+        }
+      } else {
+        *season = 0;
+      }
+
+      g_free (s);
+    }
+
+    if (episode) {
+      gchar *e = g_match_info_fetch_named (info, "episode");
+      if (e) {
+        if (*e == 'e' || *e == 'E') {
+          *episode = atoi (e + 1);
+        } else {
+          *episode = atoi (e);
+        }
+      } else {
+        *episode = 0;
+      }
+
+      g_free (e);
+    }
+
+    if (date) {
+      *date = NULL;
+    }
+
+    g_regex_unref (regex);
+    g_match_info_free (info);
+    g_free (metadata);
+
+    return;
+  }
+
+  g_regex_unref (regex);
+  g_match_info_free (info);
+
+  /* The filename doesn't look like a movie or a TV show, just use the
+     filename without extension as the title */
+  if (title) {
+    *title = g_strdelimit (metadata, ".", ' ');
+  }
+
+  if (showname) {
+    *showname = NULL;
+  }
+
+  if (date) {
+    *date = NULL;
+  }
+
+  if (season) {
+    *season = 0;
+  }
+
+  if (episode) {
+    *episode = 0;
+  }
+}
+
 static void
 got_file_info (GFile *file, GAsyncResult *result,
                GrlMetadataSourceResolveSpec *rs)
@@ -168,21 +467,99 @@ exit:
 }
 
 static void
-resolve_image (GrlMetadataSourceResolveSpec *rs)
+resolve_video (GrlMetadataSourceResolveSpec *rs, resolution_flags_t flags)
+{
+  gchar *title, *showname;
+  GDateTime *date;
+  gint season, episode;
+  GrlData *data = GRL_DATA (rs->media);
+  resolution_flags_t miss_flags = 0, fill_flags;
+
+  GRL_DEBUG ("%s",__FUNCTION__);
+
+  if (!(flags & (FLAG_VIDEO_TITLE |
+                 FLAG_VIDEO_SHOWNAME |
+                 FLAG_VIDEO_DATE |
+                 FLAG_VIDEO_SEASON |
+                 FLAG_VIDEO_EPISODE)))
+    return;
+
+  miss_flags |= grl_data_key_is_known (data, GRL_METADATA_KEY_TITLE) ?
+    0 : FLAG_VIDEO_TITLE;
+  miss_flags |= grl_data_key_is_known (data, GRL_METADATA_KEY_SHOW) ?
+    0 : FLAG_VIDEO_SHOWNAME;
+  miss_flags |= grl_data_key_is_known (data, GRL_METADATA_KEY_DATE) ?
+    0 : FLAG_VIDEO_DATE;
+  miss_flags |= grl_data_key_is_known (data, GRL_METADATA_KEY_SEASON) ?
+    0 : FLAG_VIDEO_SEASON;
+  miss_flags |= grl_data_key_is_known (data, GRL_METADATA_KEY_EPISODE) ?
+    0 : FLAG_VIDEO_EPISODE;
+
+  fill_flags = flags & miss_flags;
+
+  if (!fill_flags)
+    return;
+
+  video_guess_values_from_uri (grl_media_get_url (rs->media),
+                               &title, &showname, &date,
+                               &season, &episode);
+
+  GRL_DEBUG ("\tfound title=%s/showname=%s/year=%i/season=%i/episode=%i",
+             title, showname,
+             date != NULL ? g_date_time_get_year (date) : 0,
+             season, episode);
+
+  /* As this is just a guess, don't erase already provided values. */
+  if (title) {
+    if (fill_flags & FLAG_VIDEO_TITLE) {
+      grl_data_set_string (data, GRL_METADATA_KEY_TITLE, title);
+    }
+    g_free (title);
+  }
+
+  if (showname) {
+    if (fill_flags & FLAG_VIDEO_SHOWNAME) {
+      grl_data_set_string (data, GRL_METADATA_KEY_SHOW, showname);
+    }
+    g_free (showname);
+  }
+
+  if (date) {
+    if (fill_flags & FLAG_VIDEO_DATE) {
+      gchar *str_date = g_date_time_format (date, "%F");
+      grl_data_set_string (data, GRL_METADATA_KEY_DATE, str_date);
+      g_free (str_date);
+    }
+    g_date_time_unref (date);
+  }
+
+  if (season && (fill_flags & FLAG_VIDEO_SEASON)) {
+    grl_data_set_int (data, GRL_METADATA_KEY_SEASON, season);
+  }
+
+  if (episode && (fill_flags & FLAG_VIDEO_EPISODE)) {
+    grl_data_set_int (data, GRL_METADATA_KEY_EPISODE, episode);
+  }
+}
+
+static void
+resolve_image (GrlMetadataSourceResolveSpec *rs, resolution_flags_t flags)
 {
   GFile *file;
 
   GRL_DEBUG ("resolve_image");
 
-  file = g_file_new_for_uri (grl_media_get_url (rs->media));
+  if (flags & FLAG_THUMBNAIL) {
+    file = g_file_new_for_uri (grl_media_get_url (rs->media));
 
-  g_file_query_info_async (file, G_FILE_ATTRIBUTE_THUMBNAIL_PATH,
-                           G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, NULL,
-                           (GAsyncReadyCallback)got_file_info, rs);
+    g_file_query_info_async (file, G_FILE_ATTRIBUTE_THUMBNAIL_PATH,
+                             G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, NULL,
+                             (GAsyncReadyCallback)got_file_info, rs);
+  }
 }
 
 static void
-resolve_album_art (GrlMetadataSourceResolveSpec *rs)
+resolve_album_art (GrlMetadataSourceResolveSpec *rs, resolution_flags_t flags)
 {
   /* FIXME: implement this, according to
    * http://live.gnome.org/MediaArtStorageSpec
@@ -214,6 +591,33 @@ has_compatible_media_url (GrlMedia *media)
 
   return ret;
 }
+
+static resolution_flags_t
+get_resolution_flags (GList *keys)
+{
+  GList *key = keys;
+  resolution_flags_t flags = 0;
+
+  while (key != NULL) {
+    if (key->data == GRL_METADATA_KEY_TITLE)
+      flags |= FLAG_VIDEO_TITLE;
+    else if (key->data == GRL_METADATA_KEY_SHOW)
+      flags |= FLAG_VIDEO_SHOWNAME;
+    else if (key->data == GRL_METADATA_KEY_DATE)
+      flags |= FLAG_VIDEO_DATE;
+    else if (key->data == GRL_METADATA_KEY_SEASON)
+      flags |= FLAG_VIDEO_SEASON;
+    else if (key->data == GRL_METADATA_KEY_EPISODE)
+      flags |= FLAG_VIDEO_EPISODE;
+    else if (key->data == GRL_METADATA_KEY_THUMBNAIL)
+      flags |= FLAG_THUMBNAIL;
+
+    key = key->next;
+  }
+
+  return flags;
+}
+
 /* ================== API Implementation ================ */
 
 static const GList *
@@ -222,6 +626,11 @@ grl_local_metadata_source_supported_keys (GrlMetadataSource *source)
   static GList *keys = NULL;
   if (!keys) {
     keys = grl_metadata_key_list_new (GRL_METADATA_KEY_THUMBNAIL,
+                                      GRL_METADATA_KEY_TITLE,
+                                      GRL_METADATA_KEY_SHOW,
+                                      GRL_METADATA_KEY_DATE,
+                                      GRL_METADATA_KEY_SEASON,
+                                      GRL_METADATA_KEY_EPISODE,
                                       NULL);
   }
   return keys;
@@ -233,13 +642,31 @@ grl_local_metadata_source_may_resolve (GrlMetadataSource *source,
                                        GrlKeyID key_id,
                                        GList **missing_keys)
 {
-  if (key_id != GRL_METADATA_KEY_THUMBNAIL
-      || GRL_IS_MEDIA_AUDIO (media)
-      || GRL_IS_MEDIA_BOX (media))
-    return FALSE;
+  GrlLocalMetadataSourcePriv *priv =
+    GRL_LOCAL_METADATA_SOURCE_GET_PRIVATE (source);
+
+  if (media && grl_data_key_is_known (GRL_DATA (media),
+                                      GRL_METADATA_KEY_URL)) {
+    if (GRL_IS_MEDIA_IMAGE (media)) {
+      if (has_compatible_media_url (media) &&
+          (key_id == GRL_METADATA_KEY_THUMBNAIL))
+        return TRUE;
+    }
 
-  if (media && grl_data_key_is_known (GRL_DATA (media), GRL_METADATA_KEY_URL))
-    return has_compatible_media_url (media);
+    if (GRL_IS_MEDIA_VIDEO (media)) {
+      if (priv->guess_video &&
+          (key_id == GRL_METADATA_KEY_TITLE ||
+           key_id == GRL_METADATA_KEY_SHOW ||
+           key_id == GRL_METADATA_KEY_DATE ||
+           key_id == GRL_METADATA_KEY_SEASON ||
+           key_id == GRL_METADATA_KEY_EPISODE))
+        return TRUE;
+      if (has_compatible_media_url (media)) {
+        if (key_id == GRL_METADATA_KEY_THUMBNAIL)
+          return TRUE;
+      }
+    }
+  }
 
   if (missing_keys)
     *missing_keys = grl_metadata_key_list_new (GRL_METADATA_KEY_URL, NULL);
@@ -252,15 +679,20 @@ grl_local_metadata_source_resolve (GrlMetadataSource *source,
                                   GrlMetadataSourceResolveSpec *rs)
 {
   GError *error = NULL;
+  resolution_flags_t flags;
+  GrlLocalMetadataSourcePriv *priv =
+    GRL_LOCAL_METADATA_SOURCE_GET_PRIVATE (source);
 
   GRL_DEBUG ("grl_local_metadata_source_resolve");
 
-  if (!has_compatible_media_url (rs->media))
-    error = g_error_new (GRL_CORE_ERROR, GRL_CORE_ERROR_RESOLVE_FAILED,
-                         "local-metadata needs a url in the file:// scheme");
-  else if (!g_list_find (rs->keys, GRL_METADATA_KEY_THUMBNAIL))
-    error = g_error_new (GRL_CORE_ERROR, GRL_CORE_ERROR_RESOLVE_FAILED,
-                         "local-metadata can only resolve the thumbnail key");
+  flags = get_resolution_flags (rs->keys);
+
+   if (!flags)
+     error = g_error_new (GRL_CORE_ERROR, GRL_CORE_ERROR_RESOLVE_FAILED,
+                          "local-metadata cannot resolve any of the given keys");
+   else if (!has_compatible_media_url (rs->media))
+     error = g_error_new (GRL_CORE_ERROR, GRL_CORE_ERROR_RESOLVE_FAILED,
+                          "local-metadata needs a url in the file:// scheme");
 
   if (error) {
     /* No can do! */
@@ -269,11 +701,16 @@ grl_local_metadata_source_resolve (GrlMetadataSource *source,
     return;
   }
 
-  if (GRL_IS_MEDIA_VIDEO (rs->media)
-      || GRL_IS_MEDIA_IMAGE (rs->media)) {
-    resolve_image (rs);
+  GRL_DEBUG ("\ttrying to resolve for: %s", grl_media_get_url (rs->media));
+
+  if (GRL_IS_MEDIA_VIDEO (rs->media)) {
+    if (priv->guess_video)
+      resolve_video (rs, flags);
+    resolve_image (rs, flags);
+  } else if (GRL_IS_MEDIA_IMAGE (rs->media)) {
+    resolve_image (rs, flags);
   } else if (GRL_IS_MEDIA_AUDIO (rs->media)) {
-    resolve_album_art (rs);
+    resolve_album_art (rs, flags);
   } else {
     /* What's that media type? */
     rs->callback (source, rs->media, rs->user_data, NULL);
diff --git a/src/metadata/local-metadata/grl-local-metadata.h b/src/metadata/local-metadata/grl-local-metadata.h
index 448051e..f680061 100644
--- a/src/metadata/local-metadata/grl-local-metadata.h
+++ b/src/metadata/local-metadata/grl-local-metadata.h
@@ -51,12 +51,16 @@
                               GRL_LOCAL_METADATA_SOURCE_TYPE,    \
                               GrlLocalMetadataSourceClass))
 
-typedef struct _GrlLocalMetadataSource GrlLocalMetadataSource;
+typedef struct _GrlLocalMetadataSource     GrlLocalMetadataSource;
+typedef struct _GrlLocalMetadataSourcePriv GrlLocalMetadataSourcePriv;
 
 struct _GrlLocalMetadataSource {
 
   GrlMetadataSource parent;
 
+  /*< private >*/
+  GrlLocalMetadataSourcePriv *priv;
+
 };
 
 typedef struct _GrlLocalMetadataSourceClass GrlLocalMetadataSourceClass;



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]