# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2008 <NAME> and the gPodder Team
#
# gPodder 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.
#
# gPodder 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/>.
#


#
#  services.py -- Core Services for gPodder
#  <NAME> <<EMAIL>>   2007-08-24
#
#

from gpodder.liblogger import log
from gpodder.libgpodder import gl

from gpodder import util

import gtk
import gobject

import threading


class ObservableService(object):
    def __init__(self, signal_names=[]):
        self.observers={}
        for signal in signal_names:
            self.observers[signal]=[]

    def register(self, signal_name, observer):
        if signal_name in self.observers:
            if not observer in self.observers[signal_name]:
                self.observers[signal_name].append(observer)
            else:
                log('Observer already added to signal "%s".', signal_name, sender=self)
        else:
            log('Signal "%s" is not available for registration.', signal_name, sender=self)

    def unregister(self, signal_name, observer):
        if signal_name in self.observers:
            if observer in self.observers[signal_name]:
                self.observers[signal_name].remove(observer)
            else:
                log('Observer could not be removed from signal "%s".', signal_name, sender=self)
        else:
            log('Signal "%s" is not available for un-registration.', signal_name, sender=self)

    def notify(self, signal_name, *args):
        if signal_name in self.observers:
            for observer in self.observers[signal_name]:
                util.idle_add(observer, *args)
        else:
            log('Signal "%s" is not available for notification.', signal_name, sender=self)


class DownloadStatusManager(ObservableService):
    COLUMN_NAMES={ 0: 'episode', 1: 'speed', 2: 'progress', 3: 'url' }
    COLUMN_TYPES=( gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_FLOAT, gobject.TYPE_STRING )

    def __init__( self):
        self.status_list={}
        self.next_status_id=0

        self.last_progress_status =( 0, 0 )
        
        # use to correctly calculate percentage done
        self.downloads_done_bytes=0
        
        self.max_downloads=gl.config.max_downloads
        self.semaphore=threading.Semaphore( self.max_downloads)

        self.tree_model=gtk.ListStore( *self.COLUMN_TYPES)
        self.tree_model_lock=threading.Lock()

        # batch add in progress?
        self.batch_mode_enabled=False
        # we set this flag if we would notify inside batch mode
        self.batch_mode_notify_flag=False

        # Used to notify all threads that they should
        # re-check if they can acquire the lock
        self.notification_event=threading.Event()
        self.notification_event_waiters=0
        
        signal_names=['list-changed', 'progress-changed', 'progress-detail', 'download-complete']
        ObservableService.__init__(self, signal_names)

    def start_batch_mode(self):
        """
        This is called when we are going to add multiple
        episodes to our download list, and do not want to
        notify the GUI for every single episode.

        After all episodes have been added, you MUST call
        the end_batch_mode() method to trigger a notification.
        """
        self.batch_mode_enabled=True

    def end_batch_mode(self):
        """
        This is called after multiple episodes have been
        added when start_batch_mode() has been called before.

        This sends out a notification that the list has changed.
        """
        self.batch_mode_enabled=False
        if self.batch_mode_notify_flag:
            self.notify('list-changed')
        self.batch_mode_notify_flag=False

    def notify_progress( self):
        now=( self.count(), self.average_progress() )
        
        if now != self.last_progress_status:
            self.notify( 'progress-changed', *now)
            self.last_progress_status=now

    def s_acquire( self):
        if not gl.config.max_downloads_enabled:
            return False
        
        # Acquire queue slots if user has decreased the slots
        while self.max_downloads > gl.config.max_downloads:
            self.semaphore.acquire()
            self.max_downloads -= 1

        # Make sure we update the maximum number of downloads
        self.update_max_downloads()

        while self.semaphore.acquire(False) == False:
            self.notification_event_waiters += 1
            self.notification_event.wait(2.)
            self.notification_event_waiters -= 1

            # If we are the last thread that woke up from
            # the notification_event, clear the flag here
            if self.notification_event_waiters == 0:
                self.notification_event.clear()
                
            # If the user has change the config option since the
            # last time we checked, return false and start download
            if not gl.config.max_downloads_enabled:
                return False

        # If we land here, we've acquired exactly the one we need
        return True
    
    def update_max_downloads(self):
        # Release queue slots if user has enabled more slots
        while self.max_downloads < gl.config.max_downloads:
            self.semaphore.release()
            self.max_downloads += 1

        # Notify all threads that the limit might have been changed
        self.notification_event.set()

    def s_release( self, acquired=True):
        if acquired:
            self.semaphore.release()

    def reserve_download_id( self):
        id=self.next_status_id
	self.next_status_id=id + 1
	return id

    def remove_iter( self, iter):
        self.tree_model.remove( iter)
        return False

    def register_download_id( self, id, thread):
        self.tree_model_lock.acquire()
        self.status_list[id]={ 'iter': self.tree_model.append(), 'thread': thread, 'progress': 0.0, 'speed': _('Queued'), }
        if self.batch_mode_enabled:
            self.batch_mode_notify_flag=True
        else:
            self.notify('list-changed')
        self.tree_model_lock.release()

    def remove_download_id( self, id):
        if not id in self.status_list:
            return
        iter=self.status_list[id]['iter']
	if iter is not None:
            self.tree_model_lock.acquire()
            util.idle_add(self.remove_iter, iter)
            self.tree_model_lock.release()
            self.status_list[id]['iter']=None
            self.status_list[id]['thread'].cancel()
            del self.status_list[id]
            if not self.has_items():
                # Reset the counter now
                self.downloads_done_bytes=0
        if self.batch_mode_enabled:
            self.batch_mode_notify_flag=True
        else:
            self.notify('list-changed')
        self.notify_progress()

    def count( self):
        return len(self.status_list)

    def has_items( self):
        return self.count() > 0
    
    def average_progress( self):
        if not len(self.status_list):
            return 0

        done=sum(status['progress']/100. * status['thread'].total_size for status in self.status_list.values())
        total=sum(status['thread'].total_size for status in self.status_list.values())
        if total + self.downloads_done_bytes == 0:
            return 0
        return float(done + self.downloads_done_bytes) / float(total + self.downloads_done_bytes) * 100

    def update_status( self, id, **kwargs):
        if not id in self.status_list:
            return

        iter=self.status_list[id]['iter']
        if iter:
            self.tree_model_lock.acquire()
            for ( column, key ) in self.COLUMN_NAMES.items():
                if key in kwargs:
                    util.idle_add(self.tree_model.set, iter, column, kwargs[key])
                    self.status_list[id][key]=kwargs[key]
            self.tree_model_lock.release()

        if 'progress' in kwargs and 'speed' in kwargs and 'url' in self.status_list[id]:
            self.notify( 'progress-detail', self.status_list[id]['url'], kwargs['progress'], kwargs['speed'])

        self.notify_progress()
        
    def download_completed(self, id):
        if id in self.status_list:
            self.notify('download-complete', self.status_list[id]['episode'])
            self.downloads_done_bytes += self.status_list[id]['thread'].total_size

    def request_progress_detail( self, url):
        for status in self.status_list.values():
            if 'url' in status and status['url'] == url and 'progress' in status and 'speed' in status:
                self.notify( 'progress-detail', url, status['progress'], status['speed'])

    def is_download_in_progress( self, url):
        for element in self.status_list.keys():
            # We need this, because status_list is modified from other threads
            if element in self.status_list:
                try:
                    thread=self.status_list[element]['thread']
                except:
                    thread=None

                if thread is not None and thread.url == url:
                    return True
        
        return False

    def cancel_all( self):
        for element in self.status_list:
	    self.status_list[element]['iter']=None
	    self.status_list[element]['thread'].cancel()
        # clear the tree model after cancelling
        util.idle_add(self.tree_model.clear)
        self.downloads_done_bytes=0

    def cancel_by_url( self, url):
        for element in self.status_list:
	    thread=self.status_list[element]['thread']
	    if thread is not None and thread.url == url:
                self.remove_download_id( element)
		return True
        
        return False


download_status_manager=DownloadStatusManager()


# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2008 <NAME> and the gPodder Team
#
# gPodder 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.
#
# gPodder 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/>.
#


#
#  services.py -- Core Services for gPodder
#  <NAME> <<EMAIL>>   2007-08-24
#
#

from gpodder.liblogger import log
from gpodder.libgpodder import gl

from gpodder import util

import gtk
import gobject

import threading


class ObservableService(object):
    def __init__(self, signal_names=[]):
        self.observers={}
        for signal in signal_names:
            self.observers[signal]=[]

    def register(self, signal_name, observer):
        if signal_name in self.observers:
            if not observer in self.observers[signal_name]:
                self.observers[signal_name].append(observer)
            else:
                log('Observer already added to signal "%s".', signal_name, sender=self)
        else:
            log('Signal "%s" is not available for registration.', signal_name, sender=self)

    def unregister(self, signal_name, observer):
        if signal_name in self.observers:
            if observer in self.observers[signal_name]:
                self.observers[signal_name].remove(observer)
            else:
                log('Observer could not be removed from signal "%s".', signal_name, sender=self)
        else:
            log('Signal "%s" is not available for un-registration.', signal_name, sender=self)

    def notify(self, signal_name, *args):
        if signal_name in self.observers:
            for observer in self.observers[signal_name]:
                util.idle_add(observer, *args)
        else:
            log('Signal "%s" is not available for notification.', signal_name, sender=self)


class DownloadStatusManager(ObservableService):
    COLUMN_NAMES={ 0: 'episode', 1: 'speed', 2: 'progress', 3: 'url' }
    COLUMN_TYPES=( gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_FLOAT, gobject.TYPE_STRING )

    def __init__( self):
        self.status_list={}
        self.next_status_id=0

        self.last_progress_status =( 0, 0 )
        
        # use to correctly calculate percentage done
        self.downloads_done_bytes=0
        
        self.max_downloads=gl.config.max_downloads
        self.semaphore=threading.Semaphore( self.max_downloads)

        self.tree_model=gtk.ListStore( *self.COLUMN_TYPES)
        self.tree_model_lock=threading.Lock()

        # batch add in progress?
        self.batch_mode_enabled=False
        # we set this flag if we would notify inside batch mode
        self.batch_mode_notify_flag=False

        # Used to notify all threads that they should
        # re-check if they can acquire the lock
        self.notification_event=threading.Event()
        self.notification_event_waiters=0
        
        signal_names=['list-changed', 'progress-changed', 'progress-detail', 'download-complete']
        ObservableService.__init__(self, signal_names)

    def start_batch_mode(self):
        """
        This is called when we are going to add multiple
        episodes to our download list, and do not want to
        notify the GUI for every single episode.

        After all episodes have been added, you MUST call
        the end_batch_mode() method to trigger a notification.
        """
        self.batch_mode_enabled=True

    def end_batch_mode(self):
        """
        This is called after multiple episodes have been
        added when start_batch_mode() has been called before.

        This sends out a notification that the list has changed.
        """
        self.batch_mode_enabled=False
        if self.batch_mode_notify_flag:
            self.notify('list-changed')
        self.batch_mode_notify_flag=False

    def notify_progress( self):
        now=( self.count(), self.average_progress() )
        
        if now != self.last_progress_status:
            self.notify( 'progress-changed', *now)
            self.last_progress_status=now

    def s_acquire( self):
        if not gl.config.max_downloads_enabled:
            return False
        
        # Acquire queue slots if user has decreased the slots
        while self.max_downloads > gl.config.max_downloads:
            self.semaphore.acquire()
            self.max_downloads -= 1

        # Make sure we update the maximum number of downloads
        self.update_max_downloads()

        while self.semaphore.acquire(False) == False:
            self.notification_event_waiters += 1
            self.notification_event.wait(2.)
            self.notification_event_waiters -= 1

            # If we are the last thread that woke up from
            # the notification_event, clear the flag here
            if self.notification_event_waiters == 0:
                self.notification_event.clear()
                
            # If the user has change the config option since the
            # last time we checked, return false and start download
            if not gl.config.max_downloads_enabled:
                return False

        # If we land here, we've acquired exactly the one we need
        return True
    
    def update_max_downloads(self):
        # Release queue slots if user has enabled more slots
        while self.max_downloads < gl.config.max_downloads:
            self.semaphore.release()
            self.max_downloads += 1

        # Notify all threads that the limit might have been changed
        self.notification_event.set()

    def s_release( self, acquired=True):
        if acquired:
            self.semaphore.release()

    def reserve_download_id( self):
        id=self.next_status_id
	self.next_status_id=id + 1
	return id

    def remove_iter( self, iter):
        self.tree_model.remove( iter)
        return False

    def register_download_id( self, id, thread):
        self.tree_model_lock.acquire()
        self.status_list[id]={ 'iter': self.tree_model.append(), 'thread': thread, 'progress': 0.0, 'speed': _('Queued'), }
        if self.batch_mode_enabled:
            self.batch_mode_notify_flag=True
        else:
            self.notify('list-changed')
        self.tree_model_lock.release()

    def remove_download_id( self, id):
        if not id in self.status_list:
            return
        iter=self.status_list[id]['iter']
	if iter is not None:
            self.tree_model_lock.acquire()
            util.idle_add(self.remove_iter, iter)
            self.tree_model_lock.release()
            self.status_list[id]['iter']=None
            self.status_list[id]['thread'].cancel()
            del self.status_list[id]
            if not self.has_items():
                # Reset the counter now
                self.downloads_done_bytes=0
        if self.batch_mode_enabled:
            self.batch_mode_notify_flag=True
        else:
            self.notify('list-changed')
        self.notify_progress()

    def count( self):
        return len(self.status_list)

    def has_items( self):
        return self.count() > 0
    
    def average_progress( self):
        if not len(self.status_list):
            return 0

        done=sum(status['progress']/100. * status['thread'].total_size for status in self.status_list.values())
        total=sum(status['thread'].total_size for status in self.status_list.values())
        if total + self.downloads_done_bytes == 0:
            return 0
        return float(done + self.downloads_done_bytes) / float(total + self.downloads_done_bytes) * 100

    def update_status( self, id, **kwargs):
        if not id in self.status_list:
            return

        iter=self.status_list[id]['iter']
        if iter:
            self.tree_model_lock.acquire()
            for ( column, key ) in self.COLUMN_NAMES.items():
                if key in kwargs:
                    util.idle_add(self.tree_model.set, iter, column, kwargs[key])
                    self.status_list[id][key]=kwargs[key]
            self.tree_model_lock.release()

        if 'progress' in kwargs and 'speed' in kwargs and 'url' in self.status_list[id]:
            self.notify( 'progress-detail', self.status_list[id]['url'], kwargs['progress'], kwargs['speed'])

        self.notify_progress()
        
    def download_completed(self, id):
        if id in self.status_list:
            self.notify('download-complete', self.status_list[id]['episode'])
            self.downloads_done_bytes += self.status_list[id]['thread'].total_size

    def request_progress_detail( self, url):
        for status in self.status_list.values():
            if 'url' in status and status['url'] == url and 'progress' in status and 'speed' in status:
                self.notify( 'progress-detail', url, status['progress'], status['speed'])

    def is_download_in_progress( self, url):
        for element in self.status_list.keys():
            # We need this, because status_list is modified from other threads
            if element in self.status_list:
                try:
                    thread=self.status_list[element]['thread']
                except:
                    thread=None

                if thread is not None and thread.url == url:
                    return True
        
        return False

    def cancel_all( self):
        for element in self.status_list:
	    self.status_list[element]['iter']=None
	    self.status_list[element]['thread'].cancel()
        # clear the tree model after cancelling
        util.idle_add(self.tree_model.clear)
        self.downloads_done_bytes=0

    def cancel_by_url( self, url):
        for element in self.status_list:
	    thread=self.status_list[element]['thread']
	    if thread is not None and thread.url == url:
                self.remove_download_id( element)
		return True
        
        return False


download_status_manager=DownloadStatusManager()


# -*- coding: utf-8 -*- # # gPodder - A media aggregator and podcast client # Copyright (c) 2005-2008 <NAME> and the gPodder Team # # gPodder 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. # # gPodder 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/>. # # #  services.py -- Core Services for gPodder #  <NAME> <<EMAIL>>   2007-08-24 # # # use to correctly calculate percentage done # batch add in progress? # we set this flag if we would notify inside batch mode # Used to notify all threads that they should # re-check if they can acquire the lock This is called when we are going to add multiple         episodes to our download list, and do not want to         notify the GUI for every single episode.          After all episodes have been added, you MUST call         the end_batch_mode() method to trigger a notification. This is called after multiple episodes have been         added when start_batch_mode() has been called before.          This sends out a notification that the list has changed. # Acquire queue slots if user has decreased the slots # Make sure we update the maximum number of downloads # If we are the last thread that woke up from # the notification_event, clear the flag here # If the user has change the config option since the # last time we checked, return false and start download # If we land here, we've acquired exactly the one we need # Release queue slots if user has enabled more slots # Notify all threads that the limit might have been changed # Reset the counter now # We need this, because status_list is modified from other threads # clear the tree model after cancelling