#!/usr/bin/python2.5
# -*- coding: utf-8 -*-
# Zoutube - Youtube browser and player
# Copyright (C) 2009 Zaheer Abbas Merali <zaheerabbas at merali dot org>
# Borrowed code from: Canola2 Youtube Plugin (license of which shown below)
# Copyright (C) 2008 Instituto Nokia de Tecnologia
# Author: Adriano Rezende <adriano.rezende@openbossa.org>
#
# This program 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.
#
# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Additional permission under GNU GPL version 3 section 7
#
# If you modify this Program, or any covered work, by linking or combining it
# with Canola2 and its core components (or a modified version of any of those),
# containing parts covered by the terms of Instituto Nokia de Tecnologia End
# User Software Agreement, the licensors of this Program grant you additional
# permission to convey the resulting work.

import re
import httplib
import urllib
import urllib2
import socket
import gobject
gobject.threads_init()
import gtk
gtk.gdk.threads_init()
import hildon
import gst
from threading import Thread, Lock
import dbus
import osso

from portrait import FremantleRotation

rotation = None
try:
    from xml.etree import cElementTree as ElementTree
except ImportError:
    try:
        import cElementTree as ElementTree
    except ImportError:
	    from elementtree import ElementTree


def to_utf8(str):
    return unicode(str).decode('utf8')

class YouTube(object):
    """YouTube Backend.

    This class provides an interface to search for videos on youtube server.

    @see YouTubeEntry
    """
    url_standardfeeds = "http://gdata.youtube.com/feeds/standardfeeds"
    url_video_search = "http://gdata.youtube.com/feeds/api/videos"
    url_video_request = "http://www.youtube.com/watch?v=%s"
    url_video_request_flv = "http://www.youtube.com/get_video?video_id=%s&t=%s"
    url_categories = "http://gdata.youtube.com/schemas/2007/categories.cat"
    url_video_by_category = "http://gdata.youtube.com/feeds/videos/-"
    url_channels = "http://gdata.youtube.com/feeds/api/channels"

    def __init__(self):
        self.last_summary = {}

    def _request(self, url, *params):
        """Return feed content of a specific url."""
        self.last_url = url % params
        xml = urllib2.urlopen(url % params).read()
        self.last_summary, entries = parse_youtube_xml(xml)
        return entries

    def search(self, query):
        """Search for video by keywords."""
        return self._request("%s?vq=%s",
                             self.url_video_search, urllib2.quote(query))

    def top_rated(self):
        """Return the top rated videos."""
        return self._request("%s/top_rated", self.url_standardfeeds)

    def most_viewed(self):
        """Return the most viewed videos."""
        return self._request("%s/most_viewed", self.url_standardfeeds)

    def most_recent(self):
        """Return the most recently posted videos."""
        return self._request("%s?vq=*&orderby=published",
                             self.url_video_search)

    def recently_featured(self):
        """Return the recently featured videos."""
        return self._request("%s/recently_featured",
                             self.url_standardfeeds)

    def category_list(self):
        """Return a list of video categories."""
        xml = urllib2.urlopen(self.url_categories).read()

        tree = ElementTree.fromstring(xml)
        categories = {}
        for child in tree.getchildren():
            categories[child.get('term')] = child.get('label')
        return categories

    def video_by_category(self, category_id):
        """Return videos from a specific category."""
        return self._request("%s/%s", self.url_video_by_category,
                             category_id)

    def related_videos(self, video):
        url = video.get_related_videos_link()[1]
        return self._request(url)

    @classmethod
    def format_to_string(cls, fmt_id):
        formats = {
            22: "HD Quality",
            35: "High Quality",
            34: "Reasonable Quality",
            18: "Good Quality",
            5: "Bad Quality" }
        return formats[fmt_id]

    @classmethod
    def string_to_format(cls, s):
        formats = {
            "HD Quality": 22,
            "High Quality": 35,
            "Reasonable Quality": 34,
            "Good Quality": 18,
            "Bad Quality": 5 }
        return formats[s]

    @classmethod
    def resolve_video_url(cls, video_id):
        std_headers = {
            'User-Agent': 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1',
            'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
            'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
            'Accept-Language': 'en-us,en;q=0.5',
            'Pragma': 'no-cache',
            'Cache-Control': 'no-cache',
        }

        url = cls.url_video_request % str(video_id)
        request = urllib2.Request(url, None, std_headers)
        try:
            video_webpage = urllib2.urlopen(request).read()
        except (urllib2.URLError, httplib.HTTPException, socket.error), err:
            print "got error with url: %s %r" % (url, err,)
            return None
        # Try to find the best video format available for this video
        # (http://forum.videohelp.com/topic336882-1800.html#1912972)
        r3_exp = re.compile('.*"fmt_map"\:\s+"([^"]+)".*')
        r3=r3_exp.search(video_webpage)
        if r3:
            formats_available = urllib.unquote(r3.group(1)).split(',')
        else:
            r3_exp = re.compile('&fmt_map=([^&]+).*')
            r3 = r3_exp.search(video_webpage)
            if r3:
                formats_available = urllib.unquote(r3.group(1)).split(',')
            else:
                formats_available = []
        print "formats available: %r" % (formats_available,)
        # This is the proritized list of formats that zoutube will
        # use, depending on what is available from top to bottom.
        format_priorities = [
                '22/2000000/9/0/115', # 1280x720
                '35/640000/9/0/115',  # 640x360
                '18/512000/9/0/115',  # 480x270
                '34/0/9/0/115',       # 320x180
                '5/0/7/0/0',          # 320x180
        ]

        fmt_id = 5
        available = []
        for wanted in format_priorities:
            if wanted in formats_available:
                format, rest_ = wanted.split('/', 1)
                fmt_id = int(format)
                available.append(fmt_id)

        r2 = re.compile('.*"t"\:\s+"([^"]+)".*').search(video_webpage)
        if not r2:
            r2 = re.compile('.*&t=([^&]+)').search(video_webpage)
        if r2:
            video_real_url = 'http://www.youtube.com/get_video?video_id=' \
                + video_id + '&t=' + r2.group(1)
            return video_real_url, available
        return None, None

class InfoVideo(object):
    """Store information of a YouTube video."""

    def __init__(self, id, title):
        self.id = id
        self.title = title
        self.links = None
        self.rating = None
        self.authors = None
        self.view_count = 0
        self.thumbnails = None
        self.description = ""
        self.duration = 0

    def get_small_thumbnail(self):
        """Get the smallest thumb in size."""
        if not self.thumbnails:
            return None
        else:
            sizes = self.thumbnails.keys()
            sizes.sort()
            return self.thumbnails[sizes[0]][0]

    def get_large_thumbnail(self):
        """Get the largest thumb in size."""
        if not self.thumbnails:
            return None
        else:
            sizes = self.thumbnails.keys()
            sizes.sort()
            return self.thumbnails[sizes[-1]][0]

    def get_related_videos_link(self):
        if self.links:
            return self.links.get(
                "http://gdata.youtube.com/schemas/2007#video.related",
                None)

class InfoVideoAuthor(object):
    """Store information of a YouTube video author."""

    def __init__(self, name, uri=None, email=None):
        self.name = name
        self.uri = uri
        self.email = email


class InfoVideoRating(object):
    """Store information of a YouTube video rating."""

    def __init__(self, min=0, max=0, avg=0, num=0):
        self.min = min
        self.max = max
        self.avg = avg
        self.num = num


def get_namespaces(xml):
    space = {}
    ir = re.compile("<feed ([^>]+)")
    for c in ir.findall(xml)[0].split(' '):
        name, value = c.split("=")
        name = name.strip()
        value = value.strip()[1:-1]
        space[name] = value

    return space


def parse_youtube_xml(xml):
    """Parse an entry from youtube feed.

    Parse youtube feed and return summary and entries.
    """
    space = get_namespaces(xml)
    tree = ElementTree.fromstring(xml)

    summary = {}
    summary['total'] = int(tree.find("{%s}totalResults" % space['xmlns:openSearch']).text)
    summary['index'] = int(tree.find("{%s}startIndex" % space['xmlns:openSearch']).text)
    summary['items'] = int(tree.find("{%s}itemsPerPage" % space['xmlns:openSearch']).text)
    summary['links'] = {}

    for child in tree.findall("{%s}link" % space['xmlns']):
        rel = child.get("rel")
        if rel not in summary['links']:
            summary['links'][rel] = child.get("href")

    lst = []
    for child in tree.findall("{%s}entry" % space['xmlns']):
        id = child.find("{%s}id" % space['xmlns'])
        title = child.find("{%s}title" % space['xmlns'])

        info = InfoVideo(id=id.text.split("/")[-1],
                         title=title.text)

        info.updated = child.find('{%s}updated' % space['xmlns']).text
        info.published = child.find('{%s}published' % space['xmlns']).text

        info.links = {}
        for c in child.findall("{%s}link" % space['xmlns']):
            info.links[c.get("rel")] = ("", c.get("href"))

        info.authors = []
        for c in child.findall("{%s}author" % space['xmlns']):
            uri = c.find("{%s}uri" % space['xmlns'])
            name = c.find("{%s}name" % space['xmlns'])

            author = InfoVideoAuthor(name=name.text,
                                     uri=uri.text)
            info.authors.append(author)

        # rating
        tr = child.find("{%s}rating" % space['xmlns:gd'])
        info.rating = InfoVideoRating()
        if tr is not None:
            info.rating.min = float(tr.get("min", 0))
            info.rating.max = float(tr.get("max", 0))
            info.rating.avg = float(tr.get("average", 0))
            info.rating.num = float(tr.get("numRaters", 0))

        # viewcount
        tr = child.find("{%s}statistics" % space['xmlns:yt'])
        if tr is None:
            info.view_count = 0
        else:
            info.view_count = int(tr.get("viewCount", 0))

        # video thumbnails
        info.thumbnails = {}
        for tr in child.findall(".//{%s}group" % space['xmlns:media']):
            for description in tr.findall("{%s}description" % space['xmlns:media']):
                info.description = description.text
                if info.description:
                    info.description = info.description.replace("\n", "<br>")
                else:
                    info.description = ""
            for content in tr.findall('{%s}content' % space['xmlns:media']):
                info.duration = int(content.get('duration', 0))


            for c in tr.findall("{%s}thumbnail" % space['xmlns:media']):
                url = c.get("url")
                size = (int(c.get("width")), int(c.get("height")))
                if size not in info.thumbnails:
                    info.thumbnails[size] = [url,]
                else:
                    info.thumbnails[size].append(url)

        lst.append(info)

    return (summary, lst)

class MainScreen:

    def __init__(self):
        global rotation
        self.p = hildon.Program.get_instance()
        gtk.set_application_name("zoutube")
        self.w = hildon.StackableWindow()
        rotation = FremantleRotation("zoutube", main_window=self.w,
            version="0.1")
        self.mainvbox = gtk.VBox()
        self.pan = hildon.PannableArea()
        box = gtk.VBox()
        self.w.connect("delete-event", lambda x, y: gtk.main_quit())
        b = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT,
            hildon.BUTTON_ARRANGEMENT_VERTICAL,
            title = "Recently Featured")
        b.connect("clicked", self.on_button_clicked, "recentlyfeatured")
        box.pack_start(b, False, False, 0)
        b = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT,
            hildon.BUTTON_ARRANGEMENT_VERTICAL,
            title = "Most Viewed")
        b.connect("clicked", self.on_button_clicked, "mostviewed")
        box.pack_start(b, False, False, 0)
        b = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT,
            hildon.BUTTON_ARRANGEMENT_VERTICAL,
            title = "Most Recent")
        b.connect("clicked", self.on_button_clicked, "mostrecent")
        box.pack_start(b, False, False, 0)
        b = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT,
            hildon.BUTTON_ARRANGEMENT_VERTICAL,
            title = "Top Rated")
        b.connect("clicked", self.on_button_clicked, "toprated")
        box.pack_start(b, False, False, 0)
        self.box = box
        self.y = YouTube()
        Thread(target=self.retrieve_categories).start()
        self.pan.add_with_viewport(box)
        self.mainvbox.pack_start(self.pan, True, True, 0)
        self.w.add(self.mainvbox)
        self.w.connect("key-press-event", self.key_pressed)
        self.menu = hildon.AppMenu()
        b = gtk.Button("Search")
        b.connect_after("clicked", self.on_search_button_clicked)
        self.menu.append(b)
        b = gtk.Button("About")
        b.connect_after("clicked", self.on_about_button_clicked)
        self.menu.append(b)
        self.menu.show_all()
        self.w.set_app_menu(self.menu)


        self.w.show_all()
        hildon.hildon_gtk_window_set_progress_indicator(self.w, 1)
        self.p.add_window(self.w)
        self.searchbox = None
        self.ctx = osso.context.Context("zoutube", "0.1")

    def on_about_button_clicked(self, widget):
        about = gtk.AboutDialog()
        about.set_name("zoutube")
        about.set_comments("A Youtube browser and player for Maemo 5")
        about.set_version("0.1")
        about.set_copyright("Zaheer Abbas Merali")
        about.set_website("Http://github.com/zaheerm/zoutube")
        about.set_logo_icon_name("zoutube")
        about.connect("response", lambda x, y: x.destroy())
        about.show()

    def key_pressed(self, widget, event):
        if not self.searchbox:
            self.show_searchbox()
            self.searchbox.emit("key-press-event", event)
        return False

    def on_search_button_clicked(self, widget):
        self.show_searchbox()
        return True

    def show_searchbox(self):
        if not self.searchbox:
            self.searchbox = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
            self.searchbox.set_property('hildon-input-mode',
                gtk.HILDON_GTK_INPUT_MODE_FULL)
            self.mainvbox.pack_start(self.searchbox, False, False, 0)
            self.mainvbox.show_all()
            self.searchbox.grab_focus()
            self.searchbox.connect("activate", self.search_activated)

    def search_activated(self, widget):
        searchtext = widget.props.text
        self.mainvbox.remove(self.searchbox)
        self.searchbox = None
        if searchtext:
            videolist = YoutubeUI(self.ctx, self.y)
            Thread(target=self.search_video_list, args=(searchtext, videolist)).start()

    def retrieve_categories(self):
        try:
            c = self.y.category_list()
            gobject.idle_add(self.show_categories, c)
        except:
            gobject.idle_add(hildon.hildon_banner_show_information, self.w, '',
                "Could not connect to Youtube. Please check your network.")

    def show_categories(self, c):
        for k in c:
            v = c[k]
            b = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT,
                hildon.BUTTON_ARRANGEMENT_VERTICAL,
                title = v)
            b.connect("clicked", self.on_button_clicked, k)
            self.box.pack_start(b, False, False, 0)
        self.box.show_all()
        hildon.hildon_gtk_window_set_progress_indicator(self.w, 0)

    def on_button_clicked(self, button, data):
        videolist = YoutubeUI(self.ctx, self.y)
        Thread(target=self.retrieve_video_list, args=(data, videolist)).start()

    def retrieve_video_list(self, data, videolist):
        try:
            if data == "toprated":
                videos = self.y.top_rated()
            elif data == "mostviewed":
                videos = self.y.most_viewed()
            elif data == "mostrecent":
                videos = self.y.most_recent()
            elif data == "recentlyfeatured":
                videos = self.y.recently_featured()
            else:
                videos = self.y.video_by_category(data)
            gobject.idle_add(videolist.set_videos, videos)
        except Exception, e:
            print e
            gobject.idle_add(hildon.hildon_banner_show_information, self.w, '',
                "Could not connect to Youtube. Please check your network.")

    def search_video_list(self, searchtext, videolist):
        try:
            videos = self.y.search(searchtext)
            gobject.idle_add(videolist.set_videos, videos)
        except:
            gobject.idle_add(hildon.hildon_banner_show_information, self.w, '',
                "Could not connect to Youtube. Please check your network.")

class YoutubeUI:
    def __init__(self, ctx, y):
        # hildon has one program instance per app, so get instance
        self.p = hildon.Program.get_instance()
        self.ctx = ctx
        self.active_lock = Lock()
        self.y = y
        self.w = hildon.StackableWindow()
        self.w.connect("configure-event", self.on_configure_event)
        self.pan = hildon.PannableArea()
        self.model = gtk.ListStore(str, gtk.gdk.Pixbuf, int)
        self.iconview = hildon.GtkIconView(gtk.HILDON_UI_MODE_NORMAL)
        self.iconview.set_model(self.model)
        self.iconview.set_pixbuf_column(1)
        self.iconview.set_text_column(0)
        self.iconview.connect("item-activated", self.on_item_activated)
        self.box = gtk.VBox()
        self.buttons = []
        #self.pan.add_with_viewport(self.box)
        self.pan.add(self.iconview)
        self.w.add(self.pan)
        self.w.show_all()
        hildon.hildon_gtk_window_set_progress_indicator(self.w, 1)
        self.p.add_window(self.w)
        if self.which('gpodder'):
            self.menu = hildon.AppMenu()
            b = gtk.Button("Add this feed to gPodder")
            b.connect_after("clicked", self.on_add_feed)
            self.menu.append(b)
            self.menu.show_all()
            self.w.set_app_menu(self.menu)

    def on_add_feed(self, button):
        Thread(target=self.add_gpodder_feed).start()

    def add_gpodder_feed(self):
        import os
        os.system("gpodder --fremantle --subscribe=%s" % (self.y.last_url))

    def which(self, program):
        import os
        def is_exe(fpath):
            return os.path.exists(fpath) and os.access(fpath, os.X_OK)

        fpath, fname = os.path.split(program)
        if fpath:
            if is_exe(program):
                return program
        else:
            for path in os.environ["PATH"].split(os.pathsep):
                exe_file = os.path.join(path, program)
                if is_exe(exe_file):
                    return exe_file

        return None

    def on_configure_event(self, window, event):
        if event.height > event.width:
            self.iconview.set_columns(1)
            self.iconview.set_item_width(400)
        else:
            self.iconview.set_columns(2)
            self.iconview.set_item_width(350)

    def set_videos(self, videos):
        self.videos = videos
        Thread(target=self.retrieve_thumbnails).start()

    def retrieve_thumbnails(self):
        i = 0
        for v in self.videos:
            self.active_lock.acquire_lock()
            print "thumbnails: %r" % (v.thumbnails,)
            thumbnail = urllib2.urlopen(v.thumbnails[(120, 90)][0]).read()
            pixbufloader = gtk.gdk.PixbufLoader()
            pixbufloader.write(thumbnail)
            pixbufloader.close()
            pixbuf = pixbufloader.get_pixbuf()
            self.model.append([v.title, pixbuf, i])
            i = i + 1
            self.active_lock.release_lock()
        hildon.hildon_gtk_window_set_progress_indicator(self.w, 0)

    def on_item_activated(self, iconview, path):
        tree_iter = self.model.get_iter(path)
        v = self.videos[self.model.get_value(tree_iter, 2)]
        self.active_lock.acquire_lock()
        video = YoutubeWithMediaPlayer(v, self.ctx)
        self.active_lock.release_lock()

    def create_popup_menu(self, v):
        menu = gtk.Menu()
        download = gtk.MenuItem("Open in browser")
        download.connect("activate", self.open_browser, v)
        menu.append(download)
        menu.show_all()
        return menu

    def open_browser(self, widget, v):
        url = YouTube.url_video_request % str(v.id)
        bus = dbus.SessionBus()
        remote_object = bus.get_object("com.nokia.osso_browser", 
            "/com/nokia/osso_browser/request")
        remote_object.open_new_window(url, dbus_interface = "com.nokia.osso_browser")

class YoutubeWithMediaPlayer:

    def __init__(self, video, ctx):
        global rotation
        # hildon has one program instance per app, so get instance
        self.p = hildon.Program.get_instance()
        self.video = video
        # set name of application: this shows in titlebar
        # stackable window in case we want more windows in future in app
        self.w = hildon.StackableWindow()
        box = gtk.VBox()
        self.format_picker = hildon.PickerButton(gtk.HILDON_SIZE_AUTO, hildon.BUTTON_ARRANGEMENT_VERTICAL)
        format_selector = hildon.TouchSelector(text=True)
        result = YouTube.resolve_video_url(video.id)
        if result:
            self.url = result[0]
            self.formats = result[1]
        else:
            self.url = ""
            self.formats = []
        print "resolved"
        print "url: %r formats: %r" % (self.url, self.formats)
        current_fmt = ""
        for fmt in self.formats:
            if fmt != 22 and fmt != 35 and fmt != 34:
                current_fmt = YouTube.format_to_string(fmt)
                format_selector.append_text(YouTube.format_to_string(fmt))
        format_selector.set_active(0, 0)
        self.format_picker.set_selector(format_selector)
        self.format_picker.set_title("Select video quality")
        box.pack_start(self.format_picker, False, False, 0)
        b = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT,
            hildon.BUTTON_ARRANGEMENT_VERTICAL,
            title = "Play")
        b.connect_after("clicked", self.on_play_clicked)
        box.pack_start(b, False, False, 0)
        b = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT,
            hildon.BUTTON_ARRANGEMENT_VERTICAL,
            title = "Download")
        b.connect_after("clicked", self.on_download_clicked)
        box.pack_start(b, False, False, 0)

        textbuffer = gtk.TextBuffer()
        textbuffer.set_text(video.description)
        description = gtk.TextView(textbuffer)
        description.set_editable(False)
        description.set_wrap_mode(gtk.WRAP_WORD)
        box.pack_start(description, True, True, 0)
        self.w.add(box)
        self.w.show_all()

    def on_play_clicked(self, button):
        fmt = YouTube.string_to_format(self.format_picker.get_value())
        url = "%s&fmt=%d" % (self.url, fmt)
        self.play_url(url)

    def on_download_clicked(self, button):
        fmt = YouTube.string_to_format(self.format_picker.get_value())
        url = "%s&fmt=%d" % (self.url, fmt)
        bus = dbus.SessionBus()
        remote_object = bus.get_object("com.nokia.osso_browser",
            "/com/nokia/osso_browser/request")
        remote_object.open_new_window(url, dbus_interface = "com.nokia.osso_browser")

    def play_url(self, url):
        bus = dbus.SessionBus()
        remote_object = bus.get_object("com.nokia.mediaplayer",
            "/com/nokia/mediaplayer")
        remote_object.mime_open(url, dbus_interface = "com.nokia.mediaplayer")

if __name__ == "__main__":
    ui = MainScreen()
    gtk.main()
